js/net/http/server
js/net/http/server.ts
fino:net/http/server — HTTP server with negotiated protocol dispatch.
This module is the low-level entry point for standing up an HTTP server in
Fino. It binds a listening socket, terminates TLS when configured, sniffs or
negotiates the wire protocol per connection, and hands each request to one of
two handler shapes you supply. Everything above this — routing, middleware,
OpenAPI — lives in fino:net/http/app, which is built on top of serve().
There are two entry points. serveHttp() is the request/response
convenience: your handler receives a Request and returns a
HttpHandlerResult (a Response, or an upgraded WebSocketConnection /
WebTransport). serve() is the accept-based form: your handler receives an
IncomingHttp describing the connection attempt and must explicitly
accept() or reject() it before returning, which is what makes protocol
upgrades (WebSocket, WebTransport) first-class rather than bolted on. Both
return the same ServeServer handle.
Protocol selection is automatic. serve() wraps Socket.listen() and routes
each accepted connection to the HTTP/1.1 driver, to the HTTP/2 driver when a
plaintext connection opens with the HTTP/2 client preface (prior-knowledge
h2c), or to the HTTP/2 driver when a TLS connection negotiates h2 through
ALPN. Setting h3 additionally opens an HTTP/3 (QUIC) listener on the same
host and port. The selected driver owns per-connection keep-alive, pipelining,
and protocol-upgrade logic.
HTTP/1.1 connections are kept alive by default: the driver loops over requests
on the same TCP connection until the client sends Connection: close, the
handler returns a response carrying that header, or the connection is reset.
If a response includes neither Content-Length nor Transfer-Encoding, the
driver buffers the body eagerly and injects Content-Length; set one of those
headers yourself for a truly streaming response. If a handler throws an
unhandled error the driver replies with a bare 500 Internal Server Error and
closes the connection, so application-level error handling is the handler's
responsibility.
import { serveHttp } from 'fino:net/http/server';
const server = serveHttp({ port: 3000 }, async (req) => {
return new Response('hello');
});
console.log(`listening on ${server.port}`);
// Graceful shutdown drains in-flight connections:
await server.close();
Learn more:
- HTTP semantics: https://www.rfc-editor.org/rfc/rfc9110
- HTTP/1.1 messaging: https://www.rfc-editor.org/rfc/rfc9112
- HTTP/2: https://www.rfc-editor.org/rfc/rfc9113
- HTTP/3: https://www.rfc-editor.org/rfc/rfc9114
Types
type HttpProtocol = 'http/1.1' | 'h2' | 'h3'
Wire protocol negotiated for a connection.
'http/1.1' covers both plain TCP and TLS connections that did not negotiate
a newer protocol, 'h2' is HTTP/2 (via the plaintext preface or ALPN), and
'h3' is HTTP/3 over QUIC. The value is surfaced on HttpSession.protocol
and on each IncomingHttp, so a handler can branch on it without inspecting
the transport directly.
type HttpTransport = 'tcp' | 'tls' | 'quic'
Underlying transport carrying a connection.
'tcp' is plaintext TCP, 'tls' is a TLS-terminated TCP connection, and
'quic' is the UDP/QUIC transport used by HTTP/3. HttpSession.secure is
true for everything except 'tcp'.
type HttpHandlerResult = Response | WebSocketConnection | WebTransport
A value a handler may return for a single request.
Returning a Response sends an ordinary HTTP reply. Returning a
WebSocketConnection or WebTransport completes a protocol upgrade and hands
ownership of the connection to that object instead of writing a response body.
type HttpRequestHandler = (
request: Request,
session: HttpSession
) => HttpHandlerResult | Promise<HttpHandlerResult>
Request/response handler for serveHttp().
Called once per request with the parsed Request and the connection's
HttpSession. It returns (or resolves to) a HttpHandlerResult. An unhandled
rejection is turned into a bare 500 by the driver, so catch application
errors and return an explicit Response for them.
import { serveHttp, type HttpRequestHandler } from 'fino:net/http/server';
const handler: HttpRequestHandler = async (req, session) => {
return Response.json({ path: new URL(req.url).pathname, proto: session.protocol });
};
serveHttp({ port: 3000 }, handler);
type HttpTlsPeerInfo = TlsPeerInfo
TLS peer information exposed on HttpSession.tls.
This is an alias for the TlsPeerInfo produced by fino:net/tls, re-exported
here so consumers of the server API can name the type without importing the
TLS module directly. It carries details such as the negotiated protocol and,
under client-certificate authentication, the peer certificate.
type IncomingHttp = IncomingHttpRequest | IncomingWebSocketRequest | IncomingWebTransportRequest
The discriminated union of connection attempts delivered to a serve()
handler.
Branch on kind ('request', 'websocket', or 'webtransport') to narrow
to the concrete variant, then accept or reject it.
type ServerAcceptHandler = (incoming: IncomingHttp, session: HttpSession) => void | Promise<void>
Accept-based handler passed to serve().
Invoked once per incoming connection attempt with the IncomingHttp and its
HttpSession. The handler must resolve the incoming exactly once — accept and
respond, or reject — before it returns; failing to decide, or accepting a
request without responding, is turned into a 500 by the driver.
import { serve, type ServerAcceptHandler } from 'fino:net/http/server';
const onIncoming: ServerAcceptHandler = async (incoming) => {
switch (incoming.kind) {
case 'websocket': { const ws = await incoming.accept(); ws.send('hi'); break; }
case 'webtransport': await incoming.reject(); break;
default: await (await incoming.accept()).respond(new Response('ok'));
}
};
serve({ port: 3000 }, onIncoming);
Interfaces
interface ServeOptions {
Configuration for serve() and serveHttp().
Only port is required. Leaving hostname unset binds all interfaces
(0.0.0.0 for IPv4, :: for explicit IPv6), and port: 0 requests an
ephemeral port whose value you read back from ServeServer.port. Supplying
tls upgrades the listener to HTTPS and, when libnghttp2 is available,
advertises HTTP/2 through ALPN. Supplying h3 additionally opens an HTTP/3
listener on the same address and requires tls.
import { serveHttp } from 'fino:net/http/server';
const server = serveHttp({
port: 8443,
hostname: '127.0.0.1',
tls: { cert: '/etc/tls/cert.pem', key: '/etc/tls/key.pem' },
h3: true,
idleTimeoutMs: 30_000,
}, async () => new Response('ok'));
await server.ready;
Properties
port: number
TCP/UDP port to bind. Use 0 to request an ephemeral port.
hostname?: string
Host or interface to bind. Defaults to all interfaces (0.0.0.0 / ::).
family?: 'ipv4' | 'ipv6'
Explicit IP family for the listening socket. Defaults from hostname.
backlog?: number
Listen backlog passed through to Socket.listen().
reuseAddr?: boolean
Set SO_REUSEADDR before bind. Defaults to Socket.listen() behavior.
reusePort?: boolean
Set SO_REUSEPORT before bind where supported.
tls?: {
cert: string;
key: string;
ca?: string;
clientAuth?: 'none' | 'request' | 'require';
rejectUnauthorized?: boolean;
protocols?: readonly Extract<HttpProtocol, 'http/1.1' | 'h2'>[];
}
Enable TLS. Presence upgrades the listener to HTTPS and enables ALPN-negotiated HTTP/2.
allowH2cUpgrade?: boolean
Enable the HTTP/1.1 → h2c Upgrade dance (RFC 7540 §3.2) on plain TCP.
headersTimeoutMs?: number
HTTP/1 header timeout in milliseconds. 0 or undefined disables it.
idleTimeoutMs?: number
HTTP/1 keep-alive idle timeout in milliseconds. 0 or undefined disables it.
h3?: boolean | {
quic?: Partial<Omit<H3ServeOptions, 'port' | 'hostname' | 'certificateFile' | 'privateKeyFile'>>;
}
Enable an HTTP/3 UDP listener on the same host and port. Requires tls.
interface ServeServer {
Handle to a running server returned by serve() and serveHttp().
The server begins accepting connections immediately and keeps the event loop
alive until it is closed. It implements Symbol.asyncDispose, so an
await using binding closes it automatically when the scope exits.
import { serveHttp } from 'fino:net/http/server';
await using server = serveHttp({ port: 0 }, async () => new Response('hi'));
console.log(server.address.ip, server.port);
await server.ready;
// server.close() runs at scope exit via asyncDispose
Properties
address: {
family: string;
ip: string;
port: number;
}
The bound socket address (IP family, IP, and actual port).
Readonly Properties
readonly port: number
The actual bound port. Read this after port: 0 to learn the ephemeral port.
readonly ready: Promise<void>
Resolves when all requested listeners, including the optional HTTP/3 listener, are ready.
Methods
close(): Promise<void>
Stop accepting, close the listener, release TLS state, and resolve once in-flight connections finish. Safe to call more than once.
interface HttpSession {
Per-connection metadata shared by every request on that connection.
A session is created when a connection is dispatched and is passed to the
handler alongside each request. For accepted requests the same session object
is reachable through AcceptedHttpRequest.session, so all requests
multiplexed over one HTTP/2 or HTTP/3 connection observe an identical session.
import { serveHttp } from 'fino:net/http/server';
serveHttp({ port: 3000 }, async (_req, session) => {
const who = session.tls?.peerCertificate ? 'mTLS client' : 'anonymous';
return new Response(`${session.protocol} over ${session.transport} (${who})`);
});
Readonly Properties
readonly id: string
Process-unique identifier for this connection, e.g. http-session-7.
readonly protocol: HttpProtocol
Negotiated wire protocol for the connection.
readonly transport: HttpTransport
Underlying transport carrying the connection.
readonly secure: boolean
true for TLS and QUIC connections, false for plaintext TCP.
readonly localAddress: unknown | null
Local socket address the connection was accepted on, or null if unavailable.
readonly remoteAddress: unknown | null
Remote peer's socket address, or null if unavailable.
readonly tls: TlsPeerInfo | null
TLS peer metadata for secure transports, or null for plain TCP.
readonly closed: Promise<void>
Resolves when the connection is closed.
interface IncomingBase<TKind extends string> {
Fields shared by every kind of incoming connection attempt.
Each variant of IncomingHttp extends this base, discriminated by kind. An
incoming must be resolved exactly once — either by calling the variant's
accept() or by calling reject() — before the handler returns.
Readonly Properties
readonly kind: TKind
Discriminant identifying the variant: 'request', 'websocket', or 'webtransport'.
readonly request: Request
The parsed request that opened this connection attempt.
readonly protocol: HttpProtocol
Negotiated wire protocol for the connection.
readonly session: HttpSession
Metadata for the connection this request arrived on.
readonly tls: TlsPeerInfo | null
TLS peer metadata for secure transports, or null for plain TCP.
Methods
reject(response?: Response): Promise<void>
Decline the connection attempt. With no argument a default response is sent
(404 for requests, 400 for WebSocket upgrades); pass a Response to
control the reply. Throws if the incoming was already accepted or rejected.
interface IncomingHttpRequest extends IncomingBase<'request'> {
A plain HTTP request awaiting a decision in a serve() handler.
Call accept() to obtain an AcceptedHttpRequest you then respond() on, or
reject() to decline. This is the kind === 'request' variant of
IncomingHttp and is the common case for ordinary GET/POST traffic.
import { serve } from 'fino:net/http/server';
serve({ port: 3000 }, async (incoming) => {
if (incoming.kind !== 'request') return incoming.reject();
const accepted = await incoming.accept();
await accepted.respond(new Response('hello'));
});
Methods
accept(): Promise<AcceptedHttpRequest>
Accept the request, yielding a handle on which exactly one respond() must
be called. Throws if the incoming was already accepted or rejected.
interface AcceptedHttpRequest {
An accepted plain request, obtained from IncomingHttpRequest.accept().
Call respond() exactly once with the reply. The session here is the same
object passed to the handler, so metadata observed during acceptance stays
consistent through the response.
import { serve } from 'fino:net/http/server';
serve({ port: 3000 }, async (incoming) => {
if (incoming.kind !== 'request') return incoming.reject();
const accepted = await incoming.accept();
await accepted.respond(Response.json({ ok: true, proto: accepted.protocol }));
});
Readonly Properties
readonly kind: 'request'
Always 'request'; identifies this as an accepted plain request.
readonly request: Request
The request that was accepted.
readonly protocol: HttpProtocol
Negotiated wire protocol for the connection.
readonly session: HttpSession
The connection's session; identical to the one handed to the handler.
Methods
respond(response: HttpHandlerResult): Promise<void>
Send the reply for this request. Must be called exactly once; throws if called again.
interface IncomingWebSocketRequest extends IncomingBase<'websocket'> {
A WebSocket upgrade request awaiting a decision in a serve() handler.
Produced for HTTP/1.1 requests carrying Upgrade: websocket. Call accept()
to complete the handshake and obtain a WebSocketConnection, or reject() to
refuse (default 400). This is the kind === 'websocket' variant.
import { serve } from 'fino:net/http/server';
serve({ port: 3000 }, async (incoming) => {
if (incoming.kind !== 'websocket') return incoming.reject();
const ws = await incoming.accept({ protocol: incoming.subprotocols[0] });
ws.send('welcome');
});
Readonly Properties
readonly subprotocols: readonly string[]
Subprotocol tokens the client offered via Sec-WebSocket-Protocol.
Methods
accept(options?: WebSocketAcceptOptions): Promise<WebSocketConnection>
Complete the WebSocket handshake and take over the connection. options can
pick a negotiated subprotocol. Throws if already accepted or rejected.
interface IncomingWebTransportRequest extends IncomingBase<'webtransport'> {
An HTTP/3 extended-CONNECT request opening a WebTransport session, awaiting a
decision in a serve() handler.
Produced only for HTTP/3 connections. Call accept() to establish the
session and take over the request stream, or reject() to refuse. This is the
kind === 'webtransport' variant.
import { serve } from 'fino:net/http/server';
serve({ port: 8443, tls: { cert: 'c.pem', key: 'k.pem' }, h3: true }, async (incoming) => {
if (incoming.kind !== 'webtransport') return incoming.reject();
const wt = await incoming.accept();
const stream = await wt.createBidirectionalStream();
void stream;
});
Readonly Properties
readonly protocols: readonly string[]
Application protocol tokens requested by the client.
Methods
accept(options?: WebTransportOptions): Promise<WebTransport>
Accept the WebTransport session and take over the request stream.
Functions
function serve(options: ServeOptions, handler: ServerAcceptHandler): ServeServer
Start an accept-based HTTP server.
Each incoming connection is handled concurrently. The event loop is implicitly kept alive as long as the server is open.
hostname defaults to 0.0.0.0 for IPv4 and :: for explicit IPv6, and
port may be 0 to request an ephemeral port. When tls is present, the
server loads the certificate and key paths and advertises HTTP/2 through ALPN
when libnghttp2 is available.
close() stops accepting, closes the listening socket, releases TLS state,
and resolves after in-flight connections finish. backlog, reuseAddr, and
reusePort are passed to the underlying socket listener.
import { serve } from 'fino:net/http/server';
const server = serve({ port: 3000 }, async (incoming) => {
const accepted = await incoming.accept();
await accepted.respond(new Response('hello'));
});
console.log(server.port);
await server.close();
function serveHttp(options: ServeOptions, handler: HttpRequestHandler): ServeServer
Start a request/response HTTP server.
This is the convenience wrapper over serve(): instead of the accept-based
protocol, your handler receives the parsed Request and the connection's
HttpSession and returns a HttpHandlerResult. Ordinary requests are
accepted and responded to automatically; anything that arrives as a WebSocket
or WebTransport upgrade attempt is rejected with a default response, since
those flows require the accept-based serve() API. All listener, TLS, HTTP/2,
and HTTP/3 options behave exactly as they do for serve(), and the same
ServeServer handle is returned.
Returning a WebSocketConnection or WebTransport from the handler for a
request that was itself an upgrade is not possible here; use serve() when
you need to accept upgrades.
import { serveHttp } from 'fino:net/http/server';
const server = serveHttp({ port: 3000 }, async (req, session) => {
const url = new URL(req.url);
if (url.pathname === '/health') return new Response('ok');
return Response.json({ method: req.method, protocol: session.protocol });
});
console.log(`listening on ${server.port}`);
await server.close();