Skip to content

Commit

Permalink
feat(fetch): allow setting base urls (#1631)
Browse files Browse the repository at this point in the history
* feat(fetch): allow setting base url

* add files :|

* fix: set origin globally

* fix: add docs
  • Loading branch information
KhafraDev committed Sep 2, 2022
1 parent 3f8542b commit 9ab4967
Show file tree
Hide file tree
Showing 8 changed files with 202 additions and 3 deletions.
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;

0 comments on commit 9ab4967

Please sign in to comment.