http: add serve() with NodeRequest and NodeResponse - #65292
Conversation
Add a modern HTTP server API that uses the Fetch API's Request and
Response objects, bypassing the legacy IncomingMessage/ServerResponse
stream-based model.
Features:
- serve(options, handler) creates HTTP or HTTPS servers
- Handler receives Request, returns Response (sync or async)
- getRemoteMetadata(request) retrieves connection info
- Automatic chunked transfer encoding when no Content-Length
- Keep-alive connection support
- Graceful shutdown via AbortSignal
- Custom error handling via onError callback
The implementation reuses the battle-tested HTTPParser but creates
Fetch API objects directly from parser output. Requests are serialized
per connection (no HTTP pipelining) - parser is paused after headers
and resumed after response is written.
Example usage:
const { serve, getRemoteMetadata } = require('http');
const server = serve({}, async (request) => {
const body = await request.json();
return Response.json({ received: body });
});
server.listen(3000);
- Enable TCP_NODELAY (noDelay: true) to prevent Nagle's algorithm from batching small writes, which was causing 40ms delays - Batch header writes into a single socket.write() call - Use cork()/uncork() to batch chunked body writes These optimizations improve hello-world throughput from ~24 req/sec to ~13,600 req/sec (567x improvement). The remaining 2.3x gap vs createServer() is due to Response body streaming overhead.
Add a single frozen serverKit namespace to the bundle exports, exposing the existing fetch internals (kConstruct, HeadersList, headers guard and list accessors, request/response state accessors) that the http.serve() implementation needs to construct Request/Response subclasses without going through the public constructors. No undici logic is changed; the request and response modules only gain exports for accessors they already define. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RGGHNWBpZyJEEN1Pb2msxY
Introduce NodeRequest and NodeResponse, real subclasses of the fetch Request/Response built through undici's kConstruct path, and use them in serve(). Incoming requests skip the public Request constructor entirely: the HeadersList is filled directly from llhttp-validated parser output, the URL is parsed lazily, and the AbortSignal is only materialized on access. NodeResponse adds a fast construction path for null, string and Uint8Array bodies with no WebIDL conversion and no ReadableStream until one is requested, and writeResponse writes body sources directly instead of entering the Web Streams reader loop. Also: - request.signal now aborts and the request body stream errors when the client disconnects before the request completes, following http.Server's half-open connection semantics - socket metadata is exposed as getters on NodeRequest and cached per connection; getRemoteMetadata is unchanged - multiple Set-Cookie response headers are written as separate header lines instead of one comma-joined line - Response.error() is rejected instead of serializing as status 0 - fetch-compatible Response objects from a different undici instance are serialized through their public API - serve() is lazily loaded from lib/http.js; loading the undici bundle while http.js is still initializing let undici capture the module without maxHeaderSize, breaking fetch's default dispatcher benchmark/http/serve.js gains a serve-fast variant using NodeResponse: with wrk at c=500 it roughly doubles throughput against plain Response handlers, and plain Response handlers gain 8-16% over the previous implementation. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RGGHNWBpZyJEEN1Pb2msxY
|
Review requested:
|
|
I really implore everyone to reconsider. The fetch spec is over a decade old and has many, MANY pitfalls. Request and Response are simple classes with terrible defaults - request method normalization, methods being blocked such as |
|
@KhafraDev ... literally no one is suggesting that we adopt the fetch spec exactly as it is defined in the WHATWG fetch specification. We are also not going into this blind. We know what the pitfalls are. We know that the current specifications definitions of |
|
@mcollina ... let's keep this experimental / build-flagged until we are able to reconcile with this https://proposal-fetch-server.jasnell.me/ as it moves forward |
|
I'm not planning to land this as-is. It's in drat mode after all. |
"Good" is subjective here, I personally think a good implementation is one that follows the spec. When implementing a web spec, there are downsides and limitations that the project must accept beforehand. "You can't have your cake and eat it too". From 2022, you said that the goal is to be "as close as possible" to the fetch spec.
I trust that you personally know the limitations, but many people reading likely don't. |
Good is always subjective and specs can be changed/evolved/fixed. |
Adds
http.serve(options, handler), an HTTP server API using the Fetch Request/Response model, built directly on the HTTP parser with a dedicated parser pool.Handlers receive a
NodeRequest(subclass ofRequest) built without the public constructor: headers list filled straight from the parser, lazy URL parsing, lazyAbortSignalthat fires on client disconnect, and socket metadata getters.NodeResponse(subclass ofResponse) adds a fast construction path for string/Uint8Array/null bodies with no ReadableStream allocation unless one is requested. Both classes remain fully compatible with the fetch ecosystem (instanceof, body mixins, clone, wrapping, formData).With wrk at c=500,
NodeResponsehandlers are 20-30% faster thanhttp.createServerfor small/medium bodies; plainResponsehandlers currently trail it by ~35% (cost is in undici's public constructor).Depends on the undici serverKit surface: nodejs/undici#5690 (interim hand-patch to the bundle included here as a separate commit).
Still to do: docs.