Skip to content

Node server fetch - #6

Draft
mcollina wants to merge 4 commits into
mainfrom
node-server-fetch
Draft

Node server fetch#6
mcollina wants to merge 4 commits into
mainfrom
node-server-fetch

Conversation

@mcollina

Copy link
Copy Markdown
Owner

No description provided.

mcollina and others added 4 commits July 14, 2026 17:43
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
@mcollina
mcollina force-pushed the node-server-fetch branch from e5deca2 to 125f08f Compare July 20, 2026 06:38
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant