diff --git a/index-fetch.js b/index-fetch.js index 3d1b811daa4..ffb2ea3ab11 100644 --- a/index-fetch.js +++ b/index-fetch.js @@ -56,6 +56,9 @@ module.exports.createFastMessageEvent = createFastMessageEvent module.exports.EventSource = require('./lib/web/eventsource/eventsource').EventSource +// Exposed only through Node.js' internal Undici bundle for HTTP server use. +module.exports.serverKit = require('./lib/web/fetch/server-kit') + const api = require('./lib/api') const Dispatcher = require('./lib/dispatcher/dispatcher') Object.assign(Dispatcher.prototype, api) diff --git a/lib/web/fetch/request.js b/lib/web/fetch/request.js index dbe809c289c..1184d1f6d40 100644 --- a/lib/web/fetch/request.js +++ b/lib/web/fetch/request.js @@ -1141,5 +1141,8 @@ module.exports = { cloneRequest, getRequestDispatcher, getRequestState, + setRequestState, + setRequestHeaders, + setRequestSignal, removeRequestAbortListener } diff --git a/lib/web/fetch/response.js b/lib/web/fetch/response.js index f555ea94b15..ba19cd70fd1 100644 --- a/lib/web/fetch/response.js +++ b/lib/web/fetch/response.js @@ -635,5 +635,7 @@ module.exports = { Response, cloneResponse, fromInnerResponse, - getResponseState + getResponseState, + setResponseState, + setResponseHeaders } diff --git a/lib/web/fetch/server-kit.js b/lib/web/fetch/server-kit.js new file mode 100644 index 00000000000..b28048fb3fb --- /dev/null +++ b/lib/web/fetch/server-kit.js @@ -0,0 +1,56 @@ +'use strict' + +const { kConstruct } = require('../../core/symbols') +const { + HeadersList, + fill, + getHeadersList, + setHeadersList, + getHeadersGuard, + setHeadersGuard +} = require('./headers') +const { + getRequestState, + setRequestState, + setRequestHeaders, + setRequestSignal +} = require('./request') +const { + getResponseState, + setResponseState, + setResponseHeaders +} = require('./response') + +/** + * The single surface through which server runtimes embedding undici + * (e.g. Node.js core's http.serve()) may reach fetch internals to build + * Request/Response subclasses without going through the public + * constructors: pass kConstruct to the constructor to obtain an + * uninitialized, brand-carrying instance, then install inner state and a + * Headers wrapper through the accessors below. + * + * Invariants callers must uphold: + * - an instance created with kConstruct must have its state installed + * before it escapes + * - header names appended to a HeadersList with isLowerCase=true must be + * valid, already lowercased HTTP tokens, and values must be free of + * CR/LF/NUL (e.g. already validated by an HTTP parser) + * - installed request/response state must be shape-compatible with the + * records produced by makeRequest/makeResponse + */ +module.exports = Object.freeze({ + kConstruct, + HeadersList, + fillHeaders: fill, + getHeadersList, + setHeadersList, + getHeadersGuard, + setHeadersGuard, + getRequestState, + setRequestState, + setRequestHeaders, + setRequestSignal, + getResponseState, + setResponseState, + setResponseHeaders +}) diff --git a/test/fetch/server-kit.js b/test/fetch/server-kit.js new file mode 100644 index 00000000000..570c7d09422 --- /dev/null +++ b/test/fetch/server-kit.js @@ -0,0 +1,81 @@ +'use strict' + +const { test } = require('node:test') +const assert = require('node:assert') + +const serverKit = require('../../lib/web/fetch/server-kit') +const { Request, makeRequest } = require('../../lib/web/fetch/request') +const { Response, makeResponse } = require('../../lib/web/fetch/response') +const { Headers } = require('../../lib/web/fetch/headers') + +const { + kConstruct, + HeadersList, + setHeadersList, + setHeadersGuard, + getRequestState, + setRequestState, + setRequestHeaders, + setRequestSignal, + getResponseState, + setResponseState, + setResponseHeaders +} = serverKit + +test('is exposed through the Node.js bundle entry point', () => { + assert.strictEqual(require('../../index-fetch').serverKit, serverKit) +}) + +test('builds a working Request subclass without the public constructor', async () => { + class ServerRequest extends Request {} + + const list = new HeadersList() + list.append('x-a', 'b', true) + const state = makeRequest({ + method: 'GET', + urlList: [new URL('http://example.com/path')], + headersList: list + }) + + const controller = new AbortController() + const request = new ServerRequest(kConstruct) + setRequestState(request, state) + setRequestSignal(request, controller.signal) + const headers = new Headers(kConstruct) + setHeadersList(headers, state.headersList) + setHeadersGuard(headers, 'immutable') + setRequestHeaders(request, headers) + + assert.ok(request instanceof Request) + assert.strictEqual(getRequestState(request), state) + assert.strictEqual(request.method, 'GET') + assert.strictEqual(request.url, 'http://example.com/path') + assert.strictEqual(request.headers.get('x-a'), 'b') + assert.throws(() => request.headers.set('x-a', 'c'), TypeError) + assert.strictEqual(request.signal.aborted, false) + controller.abort() + assert.strictEqual(request.signal.aborted, true) + assert.strictEqual(await request.text(), '') +}) + +test('builds a working Response subclass without the public constructor', async () => { + class ServerResponse extends Response {} + + const state = makeResponse({ status: 201, statusText: 'Created' }) + state.headersList.append('x-b', 'c', true) + + const response = new ServerResponse(kConstruct) + setResponseState(response, state) + const headers = new Headers(kConstruct) + setHeadersList(headers, state.headersList) + setHeadersGuard(headers, 'response') + setResponseHeaders(response, headers) + + assert.ok(response instanceof Response) + assert.strictEqual(getResponseState(response), state) + assert.strictEqual(response.status, 201) + assert.strictEqual(response.statusText, 'Created') + assert.strictEqual(response.headers.get('x-b'), 'c') + assert.strictEqual(response.body, null) + assert.strictEqual(await response.text(), '') +})