NURLNURL registrynurl-lang.org →

← all packages

http

owner @Hindurable

Install

[dependencies]
http = "^0.7.9"

Versions

Dependencies (latest)

None.

http — the unified HTTP server interface for NURL

One dependency for anything that needs to serve HTTP. NURL's stdlib already ships a complete HTTP stack — sockets + TLS, a request parser, a response builder, a keep-alive server with worker pools and DoS limits, a router, static-file serving, and auth / jwt / multipart / middleware / websocket helpers. What it doesn't ship is the ~40 lines of glue every server re-invents: bind → build a router → install a shutdown signal → wrap the handler in logging / CORS / panic-recovery → run the keep-alive loop.

http is that glue, as an ergonomic, dependency-importable facade — plus the whole stdlib surface pulled in transitively, so you $-include one file and have everything a handler needs in scope.

Hello, server

$ `deps/http/src/http.nu`

@ main → i {
    : HttpApp a ( http_app_new )
    ( http_app_get a `/` \ HttpRequest req Params p → HttpResponse {
        ^ ( response_text 200 `hello` )
    } )
    ( http_app_get a `/hi/:name` \ HttpRequest req Params p → HttpResponse {
        : String o ( string_from `hi ` )
        ?? ( params_get p `name` ) { T n → { ( string_push_str o ( string_data n ) ) } F _ → {} }
        ^ ( response_text 200 ( string_data o ) )
    } )
    ^ ( http_app_listen a `127.0.0.1` 8080 )   // blocks; returns an exit code
}

Capabilities

One http_app_listen_tls call gives a server every protocol a client might ask for, chosen per connection by the client, with nothing to configure:

What the client getsHow it is chosen
HTTP/1.1keep-alive, pipelining, chunked bodies, streaming responsesthe default on both listeners
HTTP/2 (RFC 9113)multiplexed streams, HPACK, flow controlALPN h2 on the TLS listener; the connection preface on the plaintext listener (prior knowledge)
HTTP/3 (RFC 9114)QUIC (RFC 9000/9001/9002) over UDP on the same port, QPACK, 0-RTT-less 1-RTT handshakeAlt-Svc: h3 on every TCP response; a client that already knows (curl --http3, a browser on its second visit) connects over UDP directly
Post-quantum key exchangeX25519MLKEM768 (hybrid ML-KEM-768, RFC 9370 group 0x11ec)the server's first preference on TCP and QUIC alike; a client without it (pre-2024 stacks) falls back to X25519 or P-256
Post-quantum authenticationan ML-DSA (FIPS 204) certificate, CertificateVerify signed with mldsa44/65/87http_app_set_pq_cert: the ML-DSA leaf is shown to every client whose signature_algorithms lists its scheme (RFC 8446 §4.4.2.2), the classical leaf to the rest — so a post-quantum certificate never turns a client away
Classical authenticationEC P-256 (ecdsa_secp256r1_sha256) or RSA (rsa_pss_rsae_sha256) leaf, full chainthe cert / key pair of http_app_listen_tls, key form auto-detected
Session resumptionTLS 1.3 tickets, one shared ticket key per processoffered to every client that asks (psk_key_exchange_modes)

The whole stack — TCP/TLS 1.3, QUIC, HTTP/1.1, HTTP/2, HTTP/3, ML-KEM, ML-DSA, X.509 — is pure NURL over a libc socket: no OpenSSL, no nghttp2, no quiche. With both post-quantum pieces negotiated, no asymmetric operation in the handshake is one a quantum computer breaks: the traffic keys cannot be recovered from a recording, and the server cannot be impersonated. Public CAs do not issue ML-DSA certificates yet; mint a self-signed one with x509_selfsigned_mldsa (std/x509_gen.nu) or run the pki-server package as a private CA.

<!-- doctest-file-prelude $ deps/http/src/http.nu : HttpApp a ( http_app_new ) -->

( http_app_set_pq_cert a `certs/mldsa65.pem` `certs/mldsa65.key` )   // optional
^ ( http_app_listen_tls a `0.0.0.0` 443 `certs/fullchain.pem` `certs/privkey.pem` )

tcp_tls_sig_scheme conn / tcp_is_post_quantum conn (std/net.nu) report what a given connection settled on.

The App API

Construction & serving:

CallEffect
( http_app_new ) → HttpAppcreate an app (a handle: released by its last owner; http_app_free is an optional early release)
( http_app_listen a host port ) → ibind + serve until closed (SIGINT/SIGTERM drain gracefully, see http_app_drain_ms; or an error); HTTP/1.1 and HTTP/2 (prior knowledge) on the same port
( http_app_listen_tls a host port cert key ) → isame, over TLS (PEM paths; EC, RSA or ML-DSA leaf, auto-detected); ALPN h2 http/1.1, so HTTP/2-capable clients get HTTP/2; HTTP/3 (QUIC) on the same port over UDP, announced with Alt-Svc on the TCP responses
( http_app_set_http3 a 0 ) → vkeep a TLS listener TCP-only (no UDP socket, no Alt-Svc); default 1
( http_app_set_pq_cert a cert key ) → va second, post-quantum identity (ML-DSA-44/65/87 chain + PKCS#8 key) served beside the classical pair; each client is shown the one its signature_algorithms can verify — see Capabilities

Routing (handlers are ( @ HttpResponse HttpRequest Params )):

Call
( http_app_get a pattern h )also http_app_post / put / patch / delete
( http_app_route a method pattern h )any method
( http_app_use_router a router )adopt a pre-built Router (keeps it socket-testable)
( http_app_use a middleware )wrap the app's whole dispatch in a handler of your own — see below
( http_app_router a ) → Routerthe embedded router

Patterns use the stdlib router: :name captures a segment (read with params_get), *rest is a tail wildcard.

Middleware

http_app_use takes a function from the app's dispatch to the handler actually served, and is the seam for anything the built-in layers do not do — a concurrency gate in front of an expensive route, a per-IP budget, an auth check:

<!-- doctest-prelude $ stdlib/std/thread.nu @ is_heavy HttpRequest req → b { ^ F } : Semaphore gate ( sem_new 4 ) -->

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

A request reaches [alt-svc] → [log] → [cors] → your middleware → route / static, so CORS preflights and the access log still see the requests a gate holds. One middleware per app — a second call replaces the first; a chain composes inside the one closure. The app keeps its own copy of the wrapper and owns the handler it returns — nothing to keep alive or free.

Configuration (call before serving):

CallDefault
( http_app_static_dir a dir )offserve files for unmatched GET/HEAD — the path is percent-decoded before it is checked, .. and absolute paths are refused, dotfiles other than .well-known are not served, and a symlink that leads out of dir is a 404
( http_app_cors a )offpermissive CORS + 204 OPTIONS preflight
( http_app_logging a )offaccess log (method path → status) to stderr
( http_app_recover a on )—deprecated no-op: panic → 500 is an unconditional stdlib-server guarantee
( http_app_workers a n )00 = single-threaded keep-alive; n > 0 = worker pool
( http_app_idle_ms a ms )5000keep-alive idle timeout
( http_app_body_max a bytes )10 MiBrequest-body cap (parser answers 413 above it) — raise for upload endpoints
( http_app_head_max a bytes )8 KiBrequest-head cap
( http_app_max_keepalive a n )1000 threads, none asyncper-connection request reuse cap (0 = close after one); unset, http_app_async serves uncapped and the idle timeout bounds the connection
( http_app_request_timeout a ms )offper-request wall-clock budget → stock 504 on overrun
( http_app_drain_ms a ms )10000graceful shutdown on SIGTERM/SIGINT: stop accepting, close idle keep-alive connections, finish in-flight requests with Connection: close (HTTP/2 GOAWAY, HTTP/3 GOAWAY), wait up to ms, then close the rest; 0 = stop at once
( http_app_quiet a )offsuppress the startup banner

Everything under the facade is the untouched stdlib implementation, in scope from the one include: HttpRequest / HttpResponse, response_text / response_json / response_new / response_set_header / …, Router / params_get, plus the auth, jwt, multipart and websocket helpers.

Wrapping an existing router

A server that already builds a Router (e.g. to keep every route testable without a socket) drops onto the facade with one seam:

@ my_service_router → Router {
    : Router r ( router_new )
    ( router_post r `/x` \ HttpRequest req Params p → HttpResponse { ^ ( response_text 200 `x` ) } )
    ^ r
}

@ my_serve s host i port → i {
    : HttpApp a ( http_app_new )
    ( http_app_use_router a ( my_service_router ) )   // adopt the routes
    ^ ( http_app_listen a host port )                 // facade owns the glue
}

The anomaly package's HTTP service is served exactly this way.

Memory model

http_app_new returns an HttpApp handle, mutable across the registration calls. Every copy of the handle (a struct field, a Vec element, a closure capture) is the same app, and its last owner releases it — router, strings and middleware with it — so there is nothing to free; http_app_free stays as an optional early release. The embedded Router holds a stable Vec handle, so registrations accumulate correctly. http_app_listen moves the bound listener into the server and stops it on return.

Tests

./tests/http_test.sh builds tests/smoke.nu (a server exercising routing, path params, POST bodies, static fallback, CORS, and panic recovery) and drives it over curl — including that the keep-alive loop survives a handler panic.