http-client — the unified HTTP client interface library for NURL.
One include gives a consumer:
hybrid key exchange, ML-DSA and classical certificate chains), the HTTP/1.1 client, the HTTP/2 client, the RFC 6265 cookie jar, gzip / deflate — and
HttpClient facade (client.nu) that wires them into oneobject which speaks whatever the server offers.
Usage from a dependent package:
$ deps/http-client/src/http_client.nu
@ main → i { : HttpClient c ( http_client_new ) ?? ( http_client_get c https://example.org/ ) { T r → { ( nurl_print_int . r status ) } F e → { ( nurl_eprintln ( http_client_err_name e ) ) } } ^ 0 }
The intent mirrors the http package on the server side: this is THE dependency anything needing an HTTP client reaches for.
http-client/client.nu — the ergonomic HttpClient facade over the stdlib client stack.
The stdlib ships a complete client toolkit — pure-NURL TLS 1.3 (with X25519MLKEM768 hybrid key exchange and ML-DSA certificate chains), an HTTP/1.1 keep-alive client (http_pure.nu), a multiplexed HTTP/2 client (http2_client.nu), an RFC 6265 cookie jar (cookies.nu), and gzip / deflate (compress.nu). What every program re-invents is the glue that wires them into one thing: pick the protocol the server actually offers, pool the connection to reuse it, carry cookies, follow redirects, decode the body.
HttpClient collapses that into one object:
: HttpClient c ( http_client_new ) ?? ( http_client_get c https://example.org/ ) { T r → { ( nurl_print_int . r status ) } F e → { ( nurl_eprintln ( http_client_err_name e ) ) } }
Protocol selection is automatic and needs nothing configured: an https origin is dialled with ALPN "h2 http/1.1", and whichever the server picks is what the client speaks — HTTP/2 multiplexed over one connection, or HTTP/1.1 with keep-alive. A server that advertises HTTP/3 (Alt-Svc: h3=":443") gets the next request over QUIC (ext/http3_client.nu), and a QUIC attempt that fails falls back to TCP for that origin (http_client_set_h3: 0 follow Alt-Svc — the default, 1 try QUIC first, 2 never). The negotiated protocol (1, 2 or 3) and whether the key exchange was post-quantum are readable after each request (http_client_last_proto / http_client_last_pq).
Memory model: http_client_new returns an HttpClient handle. Every copy of it is the same client, and its last owner closes every pooled connection and releases the pool, the cookie jar and the rest — nothing to free (http_client_free is an optional early release). A returned HttpResponse is the caller's, dropped with its binding.
: | HttpClientErr: | HttpClientErr {
HcConnect // TCP / TLS connect failed
HcTimeout // a read or write passed the deadline
HcTls // TLS handshake or certificate verification failed
HcDns // hostname did not resolve
HcInvalidUrl // malformed URL or a scheme other than http/https
HcProtocol // the peer broke framing (bad h2/h1 response)
HcTooManyRedirects // the redirect chain passed max_redirects
HcTooLarge // the response body passed the size cap
HcDecode // a Content-Encoding body would not decompress
HcOther
}
── Errors ────────────────────────────────────────────────────────────
One error type for the whole client — the h1 transport, the h2 transport and TLS all fold into these.
@ _hc_eq_ci s a s b → bCase-insensitive ASCII equality of a header name against a lowercase literal — the client's own copy so it needs nothing file-private from http_request.nu.
@ http_client_err_name HttpClientErr e → s: HcOriginImpl: HcOriginImpl {
String key // "scheme://host:port"
String host
i port
i is_https
i proto
i has_h2 H2Client h2
i has_h1 HttpConn h1
i pq // 1 = the TLS key exchange for this origin was post-quantum
i has_h3 H3Client h3
i alt_h3_port // the port an Alt-Svc named for h3 (0 none seen)
i alt_h3_until // ... valid until this wall-clock second
i h3_failed // 1 = a QUIC attempt failed here; TCP from now on
i pq_tcp // the TCP connection's post-quantum evidence, kept beside h3's
}
── Pooled origin ─────────────────────────────────────────────────────
One live connection per (scheme, host, port). proto records what the origin negotiated: 0 nothing yet, 1 HTTP/1.1, 2 HTTP/2, 3 HTTP/3. For h2 the pooled connection is the multiplexed H2Client; for h1 it is one idle keep-alive HttpConn, present only while has_h1 is 1; for h3 it is the QUIC connection in an H3Client handle (present only while has_h3 is 1), beside whatever TCP connection the origin still holds. A connection field whose has_* is 0 holds nothing (zeroes): the record's drop releases every field it holds.
: HcOrigin { s ctl }The client's pool holds each origin as a handle on its record in an rcbox (stdlib/core/rcbox.nu); the pool is its only owner, so the record goes with the client. Its drop closes the connections it still holds — h2 and h1 are sockets the record owns, the QUIC connection is told goodbye — and the compiler then releases the fields (drop glue).
@ HcOrigin_drop sink HcOrigin h → v% Drop HcOriginImpl% Drop HcOriginImpl {
@ drop HcOriginImpl o → v {
? != . o has_h2 0 { ( h2_client_disconnect . o h2 ) } {}
? != . o has_h1 0 { ( hp_conn_close . o h1 ) } {}
? != . o has_h3 0 { ( h3_client_close . o h3 ) } {}
}
}
: HttpClientImpl: HttpClientImpl {
CookieJar jar
( Vec HcOrigin ) origins // one record per origin, in an rcbox: stable identity
i verify // verify TLS chains (1) or not (0)
i follow // follow redirects (1) or return the 3xx (0)
i max_redirects
i timeout_ms // per read/write deadline; 0 = none
i decompress // request + decode gzip/deflate (1) or leave bodies raw (0)
i body_max // response body cap in bytes; 0 = unlimited
String ua
i h3_mode // 0 follow Alt-Svc (default) · 1 try QUIC first · 2 never
// Evidence from the most recent request.
i last_proto // 1 h1, 2 h2, 3 h3, 0 none yet
i last_pq // 1 = post-quantum key exchange
}
── Client ────────────────────────────────────────────────────────────
: HttpClient { s ctl }An HttpClient is a handle on its state in an rcbox (stdlib/core/rcbox.nu): every copy is the same state, and the last owner releases it — the pool (each origin's record closes its connections), the cookie jar and the user agent.
@ HttpClient_drop sink HttpClient h → v@ http_client_get HttpClient c s url → !HttpResponse HttpClientErr@ http_client_head HttpClient c s url → !HttpResponse HttpClientErr@ http_client_delete HttpClient c s url → !HttpResponse HttpClientErr@ http_client_post HttpClient c s url ( Vec u ) body s content_type → !HttpResponse HttpClientErrPOST/PUT/PATCH with a body and a Content-Type. body is borrowed.
@ http_client_put HttpClient c s url ( Vec u ) body s content_type → !HttpResponse HttpClientErr@ http_client_patch HttpClient c s url ( Vec u ) body s content_type → !HttpResponse HttpClientErr@ http_client_post_str HttpClient c s url s body s content_type → !HttpResponse HttpClientErrPost a string body (text / JSON).
@ http_client_status HttpResponse r → i@ http_client_header HttpResponse r s name → String@ http_client_free sink HttpClient c → vLet go of c now rather than at the end of its owner's scope.