NURLNURL registrynurl-lang.org →

← http

http 0.6.0 API

http.nu

http — the unified HTTP server interface library for NURL.

One include gives a consumer both:

response builder, keep-alive server with pools & DoS limits, router, static files, auth / jwt / multipart / middleware / websocket), via the http_full aggregator; and

a running server in a few calls.

Usage from a dependent package:

$ deps/http/src/http.nu

@ main → i { : *HttpApp a ( http_app_new ) ( http_get a / \ HttpRequest req Params p → HttpResponse { ^ ( response_text 200 hello ) } ) ^ ( http_listen a 127.0.0.1 8080 ) }

The intent is that this package is THE dependency anything needing an HTTP server reaches for — complete enough to stand in for hand-wired servers such as the anomaly service or the nurlapi playground server.


app.nu

http/app.nu — the ergonomic App facade over the stdlib HTTP stack.

The stdlib already ships a complete HTTP toolkit (net sockets + TLS, request parser, response builder, keep-alive server with worker pools and DoS limits, router, static-file serving, middleware). What every server re-invents is the ~40 lines of glue that wire them together: bind → build a router → install a shutdown signal → wrap the handler in logging / CORS / panic-recovery → run the keep-alive loop.

HttpApp collapses that into one object:

: *HttpApp a ( http_app_new ) ( http_app_get a /health \ HttpRequest req Params p → HttpResponse { ^ ( response_text 200 ok ) } ) ( http_app_static_dir a ./static ) // fallback file serving ( http_app_cors a ) ( http_app_logging a ) : i rc ( http_app_listen a 127.0.0.1 8080 ) // blocks; returns exit code

Everything below the App is the untouched stdlib implementation, reached through the umbrella include in http.nu — so HttpRequest, HttpResponse, response_json, Router, Params, the auth/jwt/multipart helpers, etc. are all in scope for the handler bodies.

Memory model: http_app_new returns a heap *HttpApp (mutable across the registration calls); free it with http_app_free. The embedded Router holds a stable Vec handle, so route registrations through . a router accumulate correctly. http_app_listen MOVES the bound listener into the server and stops it on return.

API

: HttpApp

: HttpApp {
    Router router
    i idle_ms  // keep-alive idle timeout (ms) for the server
    i workers  // 0 → single-threaded server_run; >0 → server_run_pool(n)
    b use_async  // T → fiber-per-connection server_run_async (see http_app_async)
    i async_workers  // worker pthreads for the fiber runtime; 0 = one per core
    b log_requests  // access log (method/path/status) to stderr
    b cors  // permissive CORS + OPTIONS preflight
    b quiet  // suppress the "serving on host:port" banner
    b has_static  // static fallback enabled
    String webroot  // directory served for unmatched GET/HEAD when has_static
    i body_max  // request body byte cap; -1 → stdlib default (10 MiB)
    i head_max  // request head byte cap; -1 → stdlib default (8 KiB)
    i max_keepalive  // per-conn request reuse cap; -1 → server default (0 = close after one)
    i req_timeout_ms  // per-request wall-clock budget; -1 → server default (0 = disabled)
    i http3  // 1 → a TLS listener also serves HTTP/3 on the same port over UDP (default)
    String pq_cert  // optional second identity for TLS listeners: ML-DSA chain PEM ("" = none)
    String pq_key  // ...and its PKCS#8 key PEM
    // Zero or one user middleware: a function from the app's own
    // dispatch to the handler actually served. A ( Vec ) so that "none"
    // and "one" stay apart and so `http_app_use` reaches every copy of
    // the handle (see McpMiddleware).
    ( Vec HttpMiddleware ) mw
}

: HttpMiddleware

: HttpMiddleware {
    ( @ ( @ HttpResponse HttpRequest ) ( @ HttpResponse HttpRequest ) ) f
}

A user middleware, boxed so it can live in a Vec — a closure is not spellable as a generic type argument.

@ http_app_new → *HttpApp

@ http_app_free * HttpApp a → v

@ http_app_use * HttpApp a ( @ ( @ HttpResponse HttpRequest ) ( @ HttpResponse HttpRequest ) ) f → v

Wrap the app's whole dispatch — routing, static fallback and all — in a handler of your own. The facade's built-in layers (CORS, the access log, Alt-Svc) go OUTSIDE this one, so a request reaches:

[alt-svc] → [log] → [cors] → YOUR middleware → route / static

The argument is the app's dispatch; return the handler to serve instead, and call the argument wherever you want the app to run:

( http_app_use a \ ( @ HttpResponse HttpRequest ) inner → ( @ HttpResponse HttpRequest ) { ^ \ HttpRequest req → HttpResponse { ? ( heavy req ) { ( sem_acquire gate ) } {} : HttpResponse r ( inner req ) ? ( heavy req ) { ( sem_release gate ) } {} ^ r } } )

This is the seam the facade was missing. Without it a server that needs anything the built-in layers do not do — a concurrency gate in front of an expensive route, a per-IP budget, an auth check — has to abandon the facade and hand-wire router + server + shutdown itself, which is the ~40 lines the facade exists to remove. nurlapi did exactly that, for one semaphore.

One middleware. A second call replaces the first: middleware chains grow ordering questions faster than they earn them, and a chain is already expressible by composing inside the one closure.

The wrapper must outlive http_app_listen and is yours to free; the handler it RETURNS is freed by the facade with its own layers.

@ http_app_workers * HttpApp a i n → v

Serve on a worker pool of n threads (0 = single-threaded keep-alive). Each worker is pinned to one connection for that connection's whole keep-alive lifetime, so at most n clients are in flight at once — prefer http_app_async for servers that must scale past a handful of concurrent connections.

@ http_app_set_http3 * HttpApp a i on → v

HTTP/3 on TLS listeners: on by default. http_app_set_http3 a 0 keeps a TLS listener TCP-only (no UDP socket, no Alt-Svc).

@ http_app_set_pq_cert * HttpApp a s cert s key → v

A second, post-quantum identity for http_app_listen_tls: an ML-DSA (44 / 65 / 87) certificate chain and PKCS#8 key, served BESIDE the classical pair the listen call takes. Which one a connection is shown is decided per ClientHello from the client's signature_algorithms (RFC 8446 §4.4.2.2): the ML-DSA leaf for every client that lists its scheme, the classical leaf for the rest — so deploying a post-quantum certificate never turns a client away. Applies to HTTP/1.1, HTTP/2 and HTTP/3 alike (the QUIC handshake is the same TLS 1.3 code). Mint a self-signed pair with x509_selfsigned_mldsa (std/x509_gen.nu) or the pki-server package; no public CA issues ML-DSA certificates yet.

To serve ONLY an ML-DSA certificate, hand it to http_app_listen_tls directly — the key form is auto-detected.

@ http_app_async * HttpApp a i n → v

Serve fiber-per-connection on the M:N async runtime (server_run_async): every accepted connection gets its own fiber, and n worker pthreads (0 = one per core) multiplex all of them — socket waits park the fiber on the reactor instead of pinning a thread, for both plaintext and TLS listeners. This is the scaling mode; it overrides http_app_workers. Handlers must not assume a bounded number of concurrent invocations.

@ http_app_idle_ms * HttpApp a i ms → v

Keep-alive idle timeout in milliseconds (0 = server default).

@ http_app_body_max * HttpApp a i bytes → v

Request body byte cap (parser rejects larger with 413). The stdlib default is 10 MiB — raise it for upload endpoints, lower it for API-only servers.

@ http_app_head_max * HttpApp a i bytes → v

Request head byte cap (default 8 KiB).

@ http_app_max_keepalive * HttpApp a i n → v

Per-connection keep-alive request cap (0 = close after one request).

@ http_app_request_timeout * HttpApp a i ms → v

Per-request wall-clock budget in ms; overrun sends a stock 504 and closes the connection (0 = disabled).

@ http_app_recover * HttpApp a b on → v

DEPRECATED (0.3.2): panic→500 is an unconditional guarantee of the stdlib server itself — its keep-alive loop wraps every handler call (including this facade's whole dispatch + middleware chain) in recover and answers 500 on panic. The facade used to duplicate that wrapper here, which cost a throwaway 500-response build and a second recover on EVERY request for zero added safety. The knob is kept for API compatibility and is a no-op.

@ http_app_logging * HttpApp a → v

Log every request (method path → status) to stderr.

@ http_app_cors * HttpApp a → v

Permissive CORS: reflect *, answer OPTIONS preflight with 204.

@ http_app_quiet * HttpApp a → v

Suppress the startup banner on stderr.

@ http_app_static_dir * HttpApp a s dir → v

Serve files from dir for any GET/HEAD the router leaves unmatched (404). Path traversal is rejected by the underlying serve_static.

@ http_app_get * HttpApp a s pattern ( @ HttpResponse HttpRequest Params ) handler → v

@ http_app_post * HttpApp a s pattern ( @ HttpResponse HttpRequest Params ) handler → v

@ http_app_put * HttpApp a s pattern ( @ HttpResponse HttpRequest Params ) handler → v

@ http_app_patch * HttpApp a s pattern ( @ HttpResponse HttpRequest Params ) handler → v

@ http_app_delete * HttpApp a s pattern ( @ HttpResponse HttpRequest Params ) handler → v

@ http_app_route * HttpApp a s method s pattern ( @ HttpResponse HttpRequest Params ) handler → v

@ http_app_stream * HttpApp a ( @ b TcpConn HttpRequest ) f → v

Streaming routes (SSE / NDJSON / chunked). The handler sees every request BEFORE the router — return F to fall through to the normal routes, or write the whole response itself (response_begin_chunked / response_write_chunk / response_end_chunked on the TcpConn) and return T; the server then closes the connection when the handler returns (a streamed response is that connection's last — do not close the conn inside the handler). One hook per process: dispatch on . req path / . req method inside it.

@ http_app_router * HttpApp a → Router

The embedded router, for advanced use (mounting sub-routers, tests).

@ http_app_use_router * HttpApp a Router r → v

Adopt a pre-built router as the app's router, freeing the default empty one. For servers that assemble their routes elsewhere (e.g. a *_service_router → Router that stays testable without a socket): build the router, hand it to the app, and let the facade own the serving glue.

@ http_app_listen * HttpApp a s host i port → i

Bind host:port and serve until the listener is closed (SIGINT/SIGTERM or error). Returns a process exit code (0 clean, 1 on bind/serve error).

@ http_app_listen_tls * HttpApp a s host i port s cert s key → i

Same, over TLS. cert/key are PEM paths (EC, RSA or ML-DSA leaf, auto-detected; a fullchain PEM is accepted for cert). With http_app_set_pq_cert an ML-DSA pair is served beside this one.