Skip to content
Draft
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
3 changes: 3 additions & 0 deletions index-fetch.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
3 changes: 3 additions & 0 deletions lib/web/fetch/request.js
Original file line number Diff line number Diff line change
Expand Up @@ -1141,5 +1141,8 @@ module.exports = {
cloneRequest,
getRequestDispatcher,
getRequestState,
setRequestState,
setRequestHeaders,
setRequestSignal,
removeRequestAbortListener
}
4 changes: 3 additions & 1 deletion lib/web/fetch/response.js
Original file line number Diff line number Diff line change
Expand Up @@ -635,5 +635,7 @@ module.exports = {
Response,
cloneResponse,
fromInnerResponse,
getResponseState
getResponseState,
setResponseState,
setResponseHeaders
}
56 changes: 56 additions & 0 deletions lib/web/fetch/server-kit.js
Original file line number Diff line number Diff line change
@@ -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
})
81 changes: 81 additions & 0 deletions test/fetch/server-kit.js
Original file line number Diff line number Diff line change
@@ -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(), '')
})
Loading