Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(fetch): allow setting base urls #1631

Merged
merged 4 commits into from
Sep 2, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -329,6 +329,28 @@ Gets the global dispatcher used by Common API Methods.

Returns: `Dispatcher`

### `undici.setGlobalOrigin(origin)`

* origin `string | URL | undefined`

Sets the global origin used in `fetch`.

If `undefined` is passed, the global origin will be reset. This will cause `Response.redirect`, `new Request()`, and `fetch` to throw an error when a relative path is passed.

```js
setGlobalOrigin('http://localhost:3000')

const response = await fetch('/api/ping')

console.log(response.url) // http://localhost:3000/api/ping
```

### `undici.getGlobalOrigin()`

Gets the global origin used in `fetch`.

Returns: `URL`

### `UrlObject`

* **port** `string | number` (optional)
Expand Down
3 changes: 2 additions & 1 deletion index.d.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import Dispatcher = require('./types/dispatcher')
import { setGlobalDispatcher, getGlobalDispatcher } from './types/global-dispatcher'
import { setGlobalOrigin, getGlobalOrigin } from './types/global-origin'
import Pool = require('./types/pool')
import BalancedPool = require('./types/balanced-pool')
import Client = require('./types/client')
Expand All @@ -19,7 +20,7 @@ export * from './types/formdata'
export * from './types/diagnostics-channel'
export { Interceptable } from './types/mock-interceptor'

export { Dispatcher, BalancedPool, Pool, Client, buildConnector, errors, Agent, request, stream, pipeline, connect, upgrade, setGlobalDispatcher, getGlobalDispatcher, MockClient, MockPool, MockAgent, mockErrors, ProxyAgent }
export { Dispatcher, BalancedPool, Pool, Client, buildConnector, errors, Agent, request, stream, pipeline, connect, upgrade, setGlobalDispatcher, getGlobalDispatcher, setGlobalOrigin, getGlobalOrigin, MockClient, MockPool, MockAgent, mockErrors, ProxyAgent }
export default Undici

declare function Undici(url: string, opts: Pool.Options): Pool
Expand Down
5 changes: 5 additions & 0 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,11 @@ if (nodeMajor > 16 || (nodeMajor === 16 && nodeMinor >= 8)) {
module.exports.Request = require('./lib/fetch/request').Request
module.exports.FormData = require('./lib/fetch/formdata').FormData
module.exports.File = require('./lib/fetch/file').File

const { setGlobalOrigin, getGlobalOrigin } = require('./lib/fetch/global')

module.exports.setGlobalOrigin = setGlobalOrigin
module.exports.getGlobalOrigin = getGlobalOrigin
}

module.exports.request = makeDispatcher(api.request)
Expand Down
48 changes: 48 additions & 0 deletions lib/fetch/global.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
'use strict'

// In case of breaking changes, increase the version
// number to avoid conflicts.
const globalOrigin = Symbol.for('undici.globalOrigin.1')

function getGlobalOrigin () {
return globalThis[globalOrigin]
}

function setGlobalOrigin (newOrigin) {
if (
newOrigin !== undefined &&
typeof newOrigin !== 'string' &&
!(newOrigin instanceof URL)
) {
throw new Error('Invalid base url')
}

if (newOrigin === undefined) {
Object.defineProperty(globalThis, globalOrigin, {
value: undefined,
writable: true,
enumerable: false,
configurable: false
})

return
}

const parsedURL = new URL(newOrigin)

if (parsedURL.protocol !== 'http:' && parsedURL.protocol !== 'https:') {
throw new TypeError(`Only http & https urls are allowed, received ${parsedURL.protocol}`)
}

Object.defineProperty(globalThis, globalOrigin, {
value: parsedURL,
writable: true,
enumerable: false,
configurable: false
})
}

module.exports = {
getGlobalOrigin,
setGlobalOrigin
}
7 changes: 6 additions & 1 deletion lib/fetch/request.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ const {
const { kEnumerableProperty } = util
const { kHeaders, kSignal, kState, kGuard, kRealm } = require('./symbols')
const { webidl } = require('./webidl')
const { getGlobalOrigin } = require('./global')
const { kHeadersList } = require('../core/symbols')
const assert = require('assert')

Expand Down Expand Up @@ -52,7 +53,11 @@ class Request {
init = webidl.converters.RequestInit(init)

// TODO
this[kRealm] = { settingsObject: {} }
this[kRealm] = {
settingsObject: {
baseUrl: getGlobalOrigin()
}
}

// 1. Let request be null.
let request = null
Expand Down
3 changes: 2 additions & 1 deletion lib/fetch/response.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ const {
const { kState, kHeaders, kGuard, kRealm } = require('./symbols')
const { webidl } = require('./webidl')
const { FormData } = require('./formdata')
const { getGlobalOrigin } = require('./global')
const { kHeadersList } = require('../core/symbols')
const assert = require('assert')
const { types } = require('util')
Expand Down Expand Up @@ -100,7 +101,7 @@ class Response {
// TODO: base-URL?
let parsedURL
try {
parsedURL = new URL(url)
parsedURL = new URL(url, getGlobalOrigin())
} catch (err) {
throw Object.assign(new TypeError('Failed to parse URL from ' + url), {
cause: err
Expand Down
110 changes: 110 additions & 0 deletions test/fetch/relative-url.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
'use strict'

const { test, afterEach } = require('tap')
const { createServer } = require('http')
const { once } = require('events')
const {
getGlobalOrigin,
setGlobalOrigin,
Response,
Request,
fetch
} = require('../..')

afterEach(() => setGlobalOrigin(undefined))

test('setGlobalOrigin & getGlobalOrigin', (t) => {
t.equal(getGlobalOrigin(), undefined)

setGlobalOrigin('http://localhost:3000')
t.same(getGlobalOrigin(), new URL('http://localhost:3000'))

setGlobalOrigin(undefined)
t.equal(getGlobalOrigin(), undefined)

setGlobalOrigin(new URL('http://localhost:3000'))
t.same(getGlobalOrigin(), new URL('http://localhost:3000'))

t.throws(() => {
setGlobalOrigin('invalid.url')
}, TypeError)

t.throws(() => {
setGlobalOrigin('wss://invalid.protocol')
}, TypeError)

t.throws(() => setGlobalOrigin(true))

t.end()
})

test('Response.redirect', (t) => {
t.throws(() => {
Response.redirect('/relative/path', 302)
}, TypeError('Failed to parse URL from /relative/path'))

t.doesNotThrow(() => {
setGlobalOrigin('http://localhost:3000')
Response.redirect('/relative/path', 302)
})

setGlobalOrigin('http://localhost:3000')
const response = Response.redirect('/relative/path', 302)
// See step #7 of https://fetch.spec.whatwg.org/#dom-response-redirect
t.equal(response.headers.get('location'), 'http://localhost:3000/relative/path')

t.end()
})

test('new Request', (t) => {
t.throws(
() => new Request('/relative/path'),
TypeError('Failed to parse URL from /relative/path')
)

t.doesNotThrow(() => {
setGlobalOrigin('http://localhost:3000')
// eslint-disable-next-line no-new
new Request('/relative/path')
})

setGlobalOrigin('http://localhost:3000')
const request = new Request('/relative/path')
t.equal(request.url, 'http://localhost:3000/relative/path')

t.end()
})

test('fetch', async (t) => {
await t.rejects(async () => {
await fetch('/relative/path')
}, TypeError('Failed to parse URL from /relative/path'))

t.test('Basic fetch', async (t) => {
const server = createServer((req, res) => {
t.equal(req.url, '/relative/path')
res.end()
}).listen(0)

setGlobalOrigin(`http://localhost:${server.address().port}`)
t.teardown(server.close.bind(server))
await once(server, 'listening')

await t.resolves(fetch('/relative/path'))
})

t.test('fetch return', async (t) => {
const server = createServer((req, res) => {
t.equal(req.url, '/relative/path')
res.end()
}).listen(0)

setGlobalOrigin(`http://localhost:${server.address().port}`)
t.teardown(server.close.bind(server))
await once(server, 'listening')

const response = await fetch('/relative/path')

t.equal(response.url, `http://localhost:${server.address().port}/relative/path`)
})
})
7 changes: 7 additions & 0 deletions types/global-origin.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export {
setGlobalOrigin,
getGlobalOrigin
}

declare function setGlobalOrigin(origin: string | URL | undefined): void;
declare function getGlobalOrigin(): URL | undefined;