# HTTP serving `serve` from `avril:std` serves plaintext HTTP/1.1 over TCP or Unix sockets: ```ts import { serve } from "avril:std"; const server = serve({ hostname: "127.0.0.1", port: 8000 }, (request, info) => { return Response.json({ path: new URL(request.url).pathname, peer: info.remoteAddr.hostname, }); }); await server.finished; ``` `serve(handler)` defaults to `127.0.0.1:8000`. Bind to `0.0.0.0` or `[::]` explicitly for access from other hosts. Port `0` selects an available port; `server.addr` contains the actual TCP address. Binding is synchronous, so permission errors and occupied ports throw from `serve`. `onListen(addr)` runs once after binding; avril does not print a startup message. ## CLI entry modules `avril serve` owns the listener on behalf of a module that exports a default object with a `fetch(request, info)` function: ```ts import type { ServeModule } from "avril:std"; export default { fetch(request) { return new Response(new URL(request.url).pathname); }, } satisfies ServeModule; ``` ```sh avril serve -0 --hostname=127.0.0.1 --port=8000 app.ts avril serve -0 --unix=/tmp/avril.sock app.ts ``` TCP defaults to `127.0.0.1:8000`; port `0` selects an available port. `--unix` cannot be combined with `--hostname` or `--port`. The command prints its bound address to stderr. It evaluates the actual entry module, including top-level await, before validating its default export and binding. The object is preserved as `this` when calling `fetch`. The CLI listener uses host authority, not guest `net` or filesystem grants. Accepted requests and WebSocket upgrades therefore work even with `-0`. This does **not** grant guest code authority to use `fetch`, connect outbound WebSockets, bind other listeners, or access files. Without `-0`, the ordinary CLI capability defaults still apply. Package, import, environment-file, and permission options work as for `run`. Workers never inherit the CLI listener exemption. Ctrl+C (or SIGTERM on Unix) starts graceful shutdown. A second shutdown signal forcibly closes active connections, including upgraded WebSockets, and interrupts CPU-bound guest handlers. Unix socket cleanup runs when the listener stops. Unrelated guest timers and workers do not prevent the CLI from exiting after shutdown. ## Unix listeners ```ts const server = serve({ path: "/tmp/avril.sock" }, (request, info) => { return Response.json({ peer: info.remoteAddr.path }); }); ``` Programmatic Unix listeners require filesystem **write** authority at the socket path, not `net` or filesystem read authority. Relative paths resolve against the runtime's working directory. `path` cannot be combined with `hostname` or `port`. `server.addr` is `{ transport: "unix", path }`; an unnamed peer has `path: null`. HTTP URLs still use the Host header, with `localhost` as the HTTP/1.0 fallback. An existing socket, file, or symlink is never removed to make room for a listener. Cleanup checks the socket's device/inode identity before unlinking on shutdown, preserving entries that have replaced it. Cleanup does not require a fresh guest write grant. Linux pins the parent directory and created filesystem inode, using `/proc/self/fd` for binding. Other Unix platforms use ordinary path-based binding and best-effort identity-checked cleanup. That fallback can race with directory or symlink changes between authorization, binding, and cleanup; it does not provide Linux's filesystem race resistance. Non-Unix platforms do not support Unix listeners. ## Requests and responses Handlers may return a `Response` or a promise of one. Requests on different connections run concurrently, and HTTP/1.1 connections support keep-alive. `request.url` uses the client's Host header and the request target; do not trust it as a canonical origin or assume it names the listener. Request headers are immutable. `info.remoteAddr` contains the TCP or Unix peer, not an address from proxy headers. Request and response bodies use ordinary web streams with bounded native queues and backpressure, without requiring whole-body buffering. For example: ```ts serve({ port: 8000 }, (request) => new Response(request.body)); ``` The request's signal aborts when the client disconnects or the server forcibly closes its connection. A handler that wants to abandon work should observe that signal. Request bodies should be consumed during handling, not retained beyond the response's lifetime. Canceling a request body stops JavaScript reads without canceling the response. Responses obey HEAD and null-body status semantics, preserve multiple Set-Cookie headers, and encode header values as HTTP bytes. HEAD responses discard and cancel the supplied body without reading it. Hyper controls connection framing; `Connection`, `Transfer-Encoding`, `Upgrade`, `Trailer`, `Keep-Alive`, and `Proxy-Connection` response headers are rejected. Explicit Content-Length values must be valid and match buffered bodies; streamed bodies must fulfill the declared length. Custom status text is not used. Thrown handler errors, rejected promises, and invalid or already-used responses produce a generic 500 response. Without `onError`, the error is reported via the global `error` event and printed to stderr unless canceled. Supply `onError(error)` to return a custom response; if it fails, its error is reported and the generic 500 is used. Failures after response headers have been sent are also reported and close the connection rather than attempting a second response. ## File responses `serveFile(request, source, options?)` from `avril:std` returns a `Promise` for a regular file. The source may be a path, file URL, or open `FileHandle`. It works with both `avril serve` and programmatic `serve`: ```ts import { serveFile } from "avril:std"; export default { fetch(request) { return serveFile(request, new URL("./index.html", import.meta.url), { headers: { "Cache-Control": "public, max-age=60" }, }); }, }; ``` Paths may be strings or file URLs; relative strings use the runtime's working directory. Normal filesystem read authority is required, including with the CLI listener. For example, `avril serve -0 --allow-read=./index.html app.ts` grants access to that file without granting guest network authority. Passing a `FileHandle` transfers ownership immediately by default: the handle and its readable/writable streams become unusable, and the response owns cleanup, including for HEAD and other bodyless responses. The handle must be readable, open, and idle; outstanding native operations reject with `errors.Busy` without consuming it. Invalid options and already-aborted requests also leave it intact. Once ownership transfers, later preparation failures still close the file. Use `{ consume: false }` to keep a handle for repeated requests: ```ts import { open, serve, serveFile } from "avril:std"; const file = await open("./video.mp4"); const server = serve(request => serveFile(request, file, { consume: false })); await server.finished; file.close(); ``` Each response acquires its own native file reference before preparation, so closing the original does not interrupt active responses. Serving ignores the handle's current position and does not change its cursor; concurrent responses apply their own ranges independently. The original opening filename supplies the MIME hint, without reopening that path. Normal read authority is still checked on acquisition and during streaming. `consume` defaults to true and has no effect on path sources. Retained-handle serving is supported on Unix and Windows; other platforms reject `consume: false` for handles with `errors.NotSupported`. The helper supplies Content-Type from the filename (unknown extensions use `application/octet-stream`), Content-Length, Accept-Ranges, Last-Modified, and a weak ETag derived from size and modification time, when modification time is available. These validators use the opened file's metadata, not a content hash. Modification time's filesystem precision limits change detection. `options.headers` adds response headers; Content-Type and ETag override the defaults. A caller-supplied strong ETag must identify the exact file representation. Length, range, and Last-Modified headers are controlled by the helper. Protocol handling and file reads run in Rust. Returning an untouched file body from its request's handler lets the HTTP server stream it directly, without a per-chunk JS round trip. Reading, cloning, or transforming the body uses ordinary JS streams backed by the same native file reader. This currently uses buffered I/O, not `sendfile`: Hyper's byte-buffer body interface does not expose a native file-region transport. GET streams the file with bounded reads and backpressure. HEAD returns the same full-file metadata without a body and ignores Range. If-Match and If-Unmodified-Since can produce 412; If-None-Match and If-Modified-Since can produce 304, with ETag conditions taking precedence over their date counterparts. Range supports a single byte interval, open-ended interval, or suffix, producing 206 or 416. If-Range requires a matching strong ETag or exact Last-Modified date; the default weak ETag cannot satisfy it. Malformed ranges, unsupported units, and multi-range requests are ignored, returning the full representation. Missing files and non-regular paths return 404; methods other than GET and HEAD return 405 with `Allow: GET, HEAD`. Permission and other filesystem errors reject and follow the handler's normal error policy. File handles close on completion, body cancellation, request abort, or failure; consume or cancel a response body if you do not return it. Read authority is rechecked during streaming. The helper reads from the opened handle but does not snapshot the contents: publish files by atomic replacement rather than editing them in place. Growth does not extend a response beyond its original length, and premature EOF errors the body. This is not a directory server: it does not map request URLs to paths, generate indexes, or enforce a document root. If you select files from an untrusted URL, validate that selection and consider symlinks as well as `..` traversal. ## WebSocket upgrades `upgradeWebSocket` accepts an original incoming request and returns an ordinary `WebSocket` plus a special handshake response. Return that response from the same request's handler: ```ts import { upgradeWebSocket } from "avril:std"; export default { fetch(request) { const { socket, response } = upgradeWebSocket(request); socket.onmessage = (event) => socket.send(event.data); return response; }, }; ``` This also works inside programmatic `serve` handlers and over Unix sockets. The socket starts in CONNECTING; `open` fires after the 101 response has been sent. Messages, binaryType, bufferedAmount, errors, and closing use the existing WebSocket API. Incoming client frames must be masked; server frames are unmasked. Messages are limited to 16 MiB, including fragments, with bounded read queues. To select a subprotocol, pass `{ protocol: "chat" }`; it must be a valid token offered by the client. The HTTP/1.1 GET handshake, upgrade headers, version, nonce, and subprotocol selection are validated before reserving an upgrade. Outbound WebSocket capabilities are not required for accepted sockets. Check Origin and application credentials yourself; accepting a socket does not authenticate its peer. Constructed requests and clones cannot be upgraded. Preparing an upgrade twice, returning its response from another request, or reusing a completed response fails. If the upgrade response is not returned, the reserved socket receives error/close instead of keeping the server alive indefinitely. Handshake headers are immutable; permessage-deflate and other WebSocket extensions are not negotiated. ## Shutdown `server.shutdown()` stops accepting connections and waits for active responses, including streamed bodies and accepted WebSockets. It is idempotent and returns `server.finished`. Close accepted WebSockets yourself to complete graceful shutdown. Calling it inside a handler is allowed, but **do not await it there**: shutdown is waiting for that handler's response. An indefinitely running response can keep a graceful shutdown pending indefinitely. For immediate shutdown, pass an `AbortSignal` and abort it: ```ts const controller = new AbortController(); const server = serve({ port: 8000, signal: controller.signal }, handler); controller.abort(); await server.finished; ``` This also interrupts a graceful shutdown already in progress. An already-aborted signal throws its reason without opening a listener. Listener failures reject `finished`; individual malformed requests or broken connections do not terminate the server. A worker's listeners close when the worker is terminated. ## Permissions and scope Programmatic TCP listening requires `net` authority for the requested hostname and port. For port `0`, both port `0` and the selected port are checked, so a hostname-wide grant is usually appropriate. Accepting a connection and dispatching each request recheck the listener's hostname and actual port, including on existing keep-alive connections. Sending response headers, each HTTP body read/write, and preparing WebSocket upgrades also recheck that authority. Unix listeners perform the equivalent checks against write authority for their socket path. Worker listeners use the worker's narrowed capabilities. Binding a wildcard address exposes the listener on every matching interface; its grant must name that wildcard address. TLS, HTTP/2, raw connections, CONNECT tunnels, and trailers are not implemented by this API. CONNECT receives 501. Use a reverse proxy for HTTPS.