Skip to content

Commit 9ab4967

Browse files
authored
feat(fetch): allow setting base urls (nodejs#1631)
* feat(fetch): allow setting base url * add files :| * fix: set origin globally * fix: add docs
1 parent 3f8542b commit 9ab4967

File tree

8 files changed

+202
-3
lines changed

8 files changed

+202
-3
lines changed

README.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -329,6 +329,28 @@ Gets the global dispatcher used by Common API Methods.
329329

330330
Returns: `Dispatcher`
331331

332+
### `undici.setGlobalOrigin(origin)`
333+
334+
* origin `string | URL | undefined`
335+
336+
Sets the global origin used in `fetch`.
337+
338+
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.
339+
340+
```js
341+
setGlobalOrigin('http://localhost:3000')
342+
343+
const response = await fetch('/api/ping')
344+
345+
console.log(response.url) // http://localhost:3000/api/ping
346+
```
347+
348+
### `undici.getGlobalOrigin()`
349+
350+
Gets the global origin used in `fetch`.
351+
352+
Returns: `URL`
353+
332354
### `UrlObject`
333355

334356
* **port** `string | number` (optional)

index.d.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import Dispatcher = require('./types/dispatcher')
22
import { setGlobalDispatcher, getGlobalDispatcher } from './types/global-dispatcher'
3+
import { setGlobalOrigin, getGlobalOrigin } from './types/global-origin'
34
import Pool = require('./types/pool')
45
import BalancedPool = require('./types/balanced-pool')
56
import Client = require('./types/client')
@@ -19,7 +20,7 @@ export * from './types/formdata'
1920
export * from './types/diagnostics-channel'
2021
export { Interceptable } from './types/mock-interceptor'
2122

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

2526
declare function Undici(url: string, opts: Pool.Options): Pool

index.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,11 @@ if (nodeMajor > 16 || (nodeMajor === 16 && nodeMinor >= 8)) {
104104
module.exports.Request = require('./lib/fetch/request').Request
105105
module.exports.FormData = require('./lib/fetch/formdata').FormData
106106
module.exports.File = require('./lib/fetch/file').File
107+
108+
const { setGlobalOrigin, getGlobalOrigin } = require('./lib/fetch/global')
109+
110+
module.exports.setGlobalOrigin = setGlobalOrigin
111+
module.exports.getGlobalOrigin = getGlobalOrigin
107112
}
108113

109114
module.exports.request = makeDispatcher(api.request)

lib/fetch/global.js

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
'use strict'
2+
3+
// In case of breaking changes, increase the version
4+
// number to avoid conflicts.
5+
const globalOrigin = Symbol.for('undici.globalOrigin.1')
6+
7+
function getGlobalOrigin () {
8+
return globalThis[globalOrigin]
9+
}
10+
11+
function setGlobalOrigin (newOrigin) {
12+
if (
13+
newOrigin !== undefined &&
14+
typeof newOrigin !== 'string' &&
15+
!(newOrigin instanceof URL)
16+
) {
17+
throw new Error('Invalid base url')
18+
}
19+
20+
if (newOrigin === undefined) {
21+
Object.defineProperty(globalThis, globalOrigin, {
22+
value: undefined,
23+
writable: true,
24+
enumerable: false,
25+
configurable: false
26+
})
27+
28+
return
29+
}
30+
31+
const parsedURL = new URL(newOrigin)
32+
33+
if (parsedURL.protocol !== 'http:' && parsedURL.protocol !== 'https:') {
34+
throw new TypeError(`Only http & https urls are allowed, received ${parsedURL.protocol}`)
35+
}
36+
37+
Object.defineProperty(globalThis, globalOrigin, {
38+
value: parsedURL,
39+
writable: true,
40+
enumerable: false,
41+
configurable: false
42+
})
43+
}
44+
45+
module.exports = {
46+
getGlobalOrigin,
47+
setGlobalOrigin
48+
}

lib/fetch/request.js

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ const {
2323
const { kEnumerableProperty } = util
2424
const { kHeaders, kSignal, kState, kGuard, kRealm } = require('./symbols')
2525
const { webidl } = require('./webidl')
26+
const { getGlobalOrigin } = require('./global')
2627
const { kHeadersList } = require('../core/symbols')
2728
const assert = require('assert')
2829

@@ -52,7 +53,11 @@ class Request {
5253
init = webidl.converters.RequestInit(init)
5354

5455
// TODO
55-
this[kRealm] = { settingsObject: {} }
56+
this[kRealm] = {
57+
settingsObject: {
58+
baseUrl: getGlobalOrigin()
59+
}
60+
}
5661

5762
// 1. Let request be null.
5863
let request = null

lib/fetch/response.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ const {
2121
const { kState, kHeaders, kGuard, kRealm } = require('./symbols')
2222
const { webidl } = require('./webidl')
2323
const { FormData } = require('./formdata')
24+
const { getGlobalOrigin } = require('./global')
2425
const { kHeadersList } = require('../core/symbols')
2526
const assert = require('assert')
2627
const { types } = require('util')
@@ -100,7 +101,7 @@ class Response {
100101
// TODO: base-URL?
101102
let parsedURL
102103
try {
103-
parsedURL = new URL(url)
104+
parsedURL = new URL(url, getGlobalOrigin())
104105
} catch (err) {
105106
throw Object.assign(new TypeError('Failed to parse URL from ' + url), {
106107
cause: err

test/fetch/relative-url.js

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
'use strict'
2+
3+
const { test, afterEach } = require('tap')
4+
const { createServer } = require('http')
5+
const { once } = require('events')
6+
const {
7+
getGlobalOrigin,
8+
setGlobalOrigin,
9+
Response,
10+
Request,
11+
fetch
12+
} = require('../..')
13+
14+
afterEach(() => setGlobalOrigin(undefined))
15+
16+
test('setGlobalOrigin & getGlobalOrigin', (t) => {
17+
t.equal(getGlobalOrigin(), undefined)
18+
19+
setGlobalOrigin('http://localhost:3000')
20+
t.same(getGlobalOrigin(), new URL('http://localhost:3000'))
21+
22+
setGlobalOrigin(undefined)
23+
t.equal(getGlobalOrigin(), undefined)
24+
25+
setGlobalOrigin(new URL('http://localhost:3000'))
26+
t.same(getGlobalOrigin(), new URL('http://localhost:3000'))
27+
28+
t.throws(() => {
29+
setGlobalOrigin('invalid.url')
30+
}, TypeError)
31+
32+
t.throws(() => {
33+
setGlobalOrigin('wss://invalid.protocol')
34+
}, TypeError)
35+
36+
t.throws(() => setGlobalOrigin(true))
37+
38+
t.end()
39+
})
40+
41+
test('Response.redirect', (t) => {
42+
t.throws(() => {
43+
Response.redirect('/relative/path', 302)
44+
}, TypeError('Failed to parse URL from /relative/path'))
45+
46+
t.doesNotThrow(() => {
47+
setGlobalOrigin('http://localhost:3000')
48+
Response.redirect('/relative/path', 302)
49+
})
50+
51+
setGlobalOrigin('http://localhost:3000')
52+
const response = Response.redirect('/relative/path', 302)
53+
// See step #7 of https://fetch.spec.whatwg.org/#dom-response-redirect
54+
t.equal(response.headers.get('location'), 'http://localhost:3000/relative/path')
55+
56+
t.end()
57+
})
58+
59+
test('new Request', (t) => {
60+
t.throws(
61+
() => new Request('/relative/path'),
62+
TypeError('Failed to parse URL from /relative/path')
63+
)
64+
65+
t.doesNotThrow(() => {
66+
setGlobalOrigin('http://localhost:3000')
67+
// eslint-disable-next-line no-new
68+
new Request('/relative/path')
69+
})
70+
71+
setGlobalOrigin('http://localhost:3000')
72+
const request = new Request('/relative/path')
73+
t.equal(request.url, 'http://localhost:3000/relative/path')
74+
75+
t.end()
76+
})
77+
78+
test('fetch', async (t) => {
79+
await t.rejects(async () => {
80+
await fetch('/relative/path')
81+
}, TypeError('Failed to parse URL from /relative/path'))
82+
83+
t.test('Basic fetch', async (t) => {
84+
const server = createServer((req, res) => {
85+
t.equal(req.url, '/relative/path')
86+
res.end()
87+
}).listen(0)
88+
89+
setGlobalOrigin(`http://localhost:${server.address().port}`)
90+
t.teardown(server.close.bind(server))
91+
await once(server, 'listening')
92+
93+
await t.resolves(fetch('/relative/path'))
94+
})
95+
96+
t.test('fetch return', async (t) => {
97+
const server = createServer((req, res) => {
98+
t.equal(req.url, '/relative/path')
99+
res.end()
100+
}).listen(0)
101+
102+
setGlobalOrigin(`http://localhost:${server.address().port}`)
103+
t.teardown(server.close.bind(server))
104+
await once(server, 'listening')
105+
106+
const response = await fetch('/relative/path')
107+
108+
t.equal(response.url, `http://localhost:${server.address().port}/relative/path`)
109+
})
110+
})

types/global-origin.d.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
export {
2+
setGlobalOrigin,
3+
getGlobalOrigin
4+
}
5+
6+
declare function setGlobalOrigin(origin: string | URL | undefined): void;
7+
declare function getGlobalOrigin(): URL | undefined;

0 commit comments

Comments
 (0)