The runtime adapters wrap the same app.fetch(Request) dispatch for each platform. They are available only as subpaths (never from the root barrel), so runtime-specific code such as node:http never leaks into an edge or Worker bundle. For guide-level setup per platform, see the adapters overview. For the module map, see the API reference overview.
same behaviorResponseidentical headers, errors, and docs everywhere
Adapters translate each platform's entrypoint into the same web-standard dispatch, so behavior stays identical across runtimes.
@daloyjs/core/node
ts
serve(app: App, opts?: NodeServerOptions): NodeServerHandle;interface NodeServerOptions { port?: number; // default: 3000 hostname?: string; // default: "0.0.0.0" connectionTimeoutMs?: number; // default: 30_000 shutdownTimeoutMs?: number; // default: 10_000 handleSignals?: boolean; // default: true (SIGINT/SIGTERM) maxHeaderBytes?: number; // default: 16 KiB trustProxy?: boolean; // honor x-forwarded-proto/host (only behind a trusted LB); proto only as http/https maxConnections?: number; // cap concurrent sockets (admission control); default: unset (unbounded) bufferedBodyMaxBytes?: number; // default: 256 KiB (pre-buffer threshold for POST hot path)}interface NodeServerHandle { server: Server; port: number; close(): Promise<void> }// Request-target hardening: raw %2e%2e dot segments are resolved and \ is// folded to / before routing, and a Host (or trusted X-Forwarded-Host) that// contains \ / ? # @ % or whitespace is refused with 400. When the client// disconnects mid-response, a streamed body (SSE, NDJSON) is cancelled.
Pass port: 0 when a test needs an ephemeral port. Because Node binds asynchronously, wait for the server's listening event before reading handle.port. It then reports the OS-assigned port instead of 0.
ts
import { once } from "node:events";const handle = serve(app, { port: 0, handleSignals: false });await once(handle.server, "listening");const baseUrl = `http://127.0.0.1:${handle.port}`;
Cancellation with request.signal
On Node, ctx.request.signal aborts in two cases:
Request timeout. When requestTimeoutMs elapses, the reason is a TimeoutError.
Client disconnect. When the client hangs up before the response finished, the reason is an AbortError.
Both cases apply to GET requests, small pre-buffered bodies, and streamed bodies. Bun and Deno abort the platform's own Request.signal on disconnect, so the same handler code works there too. Forward the signal to anything that costs money or holds resources:
ts
app.post("/ask", contract, async ({ body, request }) => { // Stops the model call if the user closes the tab or the request times out. const { text } = await generateText({ model, prompt: body.prompt, abortSignal: request.signal, }); return { status: 200 as const, body: { answer: text } };});
The adapter attaches its disconnect listener only when a handler reads signal, so routes that never read it pay nothing. A response that finished normally never aborts the signal afterwards. Work that fails because of a disconnect is logged as request.disconnected with disconnectStatusCode (default 499), not as a 5xx.
Pass the fetch event's waitUntil so the OTLP telemetry flush can finish after the response; without it the flush is fire-and-forget.
@daloyjs/core/lambda
ts
toLambdaHandler(app: App): LambdaHandler;toLambdaStreamHandler(app: App): LambdaStreamHandler;type LambdaHandler = (event: LambdaEvent) => Promise<LambdaResponse>;type LambdaStreamHandler = (event: LambdaEvent, responseStream: LambdaResponseStream, context?: unknown) => Promise<void>;type LambdaEvent = LambdaEventV1 | LambdaEventV2; // API Gateway REST + HTTP/Function URLstype LambdaResponse = LambdaResponseV1 | LambdaResponseV2;// A Host that is not a plain host[:port] or [IPv6][:port] (for example one// containing \ / ? # @ % or whitespace) is refused with 400.// X-Forwarded-Proto is honoured only as http or https; anything else is https.
@daloyjs/core/azure
ts
toAzureHandler(app: App, opts?: AzureHandlerOptions): AzureHttpHandler; // azure.http("api", { route: "{*path}", methods: [...], handler: toAzureHandler(app) })interface AzureHandlerOptions { stream?: boolean; // return the body as a ReadableStream; pair with app.setup({ enableHttpStream: true })}type AzureHttpHandler = (request: AzureHttpRequestLike, context?: unknown) => Promise<AzureHttpResponseInit>;interface AzureHttpResponseInit { status: number; headers: Record<string, string>; // every header except set-cookie cookies?: AzureCookie[]; // one entry per Set-Cookie body?: ReadableStream<Uint8Array> | ArrayBuffer;}// The request body is passed in as a stream, so bodyLimitBytes is enforced// while reading. An unparseable request URL is answered with 400.// Partitioned / Priority cookie attributes have no Azure field and are dropped.// Azure exposes no TCP peer: getConnInfo() is undefined; use behindProxy.
PropertyTypeDefault
stream
boolean
Default:-
Return the response body as a ReadableStream instead of buffering it. Enable this only together with app.setup({ enableHttpStream: true }) from @azure/functions; without that host setting Azure buffers the stream anyway. Default: false.
These are exported for internal tests and tooling. They are public-typed but underscore-prefixed. They may change without a semver bump. Most application code will never need them.