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
? H2Client h2
? HttpConn h1
i pq // 1 = the TLS key exchange for this origin was post-quantum
? 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 it is not lent to a request; for h3 it is the QUIC connection in an H3Client handle, beside whatever TCP connection the origin still holds. Each is an option: an absent connection is F, never a made-up handle.
% Drop HcOriginImpl% Drop HcOriginImpl {
@ drop HcOriginImpl o → v {
?? . o h2 { T c → { ( h2_client_disconnect c ) } F → {} }
?? . o h1 { T c → { ( hp_conn_close c ) } F → {} }
?? . o h3 { T c → { ( h3_client_close c ) } F → {} }
}
}
An origin's record closes the connections it still holds when it goes (the pool is its only owner, so it goes with the client): h2 and h1 are sockets the record owns, the QUIC connection is told goodbye. The compiler then releases the fields (drop glue).
: HttpClientImpl: HttpClientImpl {
CookieJar jar
( Vec HcOriginImpl ) origins // one record per origin; a request checks its origin's out
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 { ( Locked HttpClientImpl ) l }An HttpClient is a handle on its state in a Locked (stdlib/std/thread.nu): every copy is the same client, on any thread, and the last owner releases it — the pool (each origin's record closes its connections), the cookie jar and the user agent. A request checks the state out for its length, so requests through one client run one at a time (they share its connections and its jar); a client per thread runs them in parallel.
@ HttpClient_share HttpClient h → HttpClient@ HttpClient_drop sink HttpClient h → v@ http_client_new → HttpClient@ http_client_set_verify HttpClient c__h b on → vSkip TLS certificate / hostname verification. For pinned, self-signed or test servers only — an unverified connection authenticates nothing.
@ http_client_set_follow HttpClient c__h b on → vFollow 3xx redirects (default) or hand the 3xx response back.
@ http_client_set_max_redirects HttpClient c__h i n → v@ http_client_set_timeout HttpClient c__h i ms → vPer read/write deadline in milliseconds (0 = none). A stalled server answers HcTimeout instead of hanging.
@ http_client_set_decompress HttpClient c__h b on → vOffer and decode gzip/deflate bodies (default) or leave the body as the wire carried it (the caller then owns Content-Encoding).
@ http_client_set_body_max HttpClient c__h i n → vCap the (decoded) response body; a larger body answers HcTooLarge.
@ http_client_set_user_agent HttpClient c__h s ua → v@ http_client_set_h3 HttpClient c__h i mode → vThe protocol / key-exchange facts of the most recent request. HTTP/3: 0 (default) use QUIC once an origin's response has advertised it with Alt-Svc: h3=...; 1 try QUIC first on every https origin (falling back to TCP when the attempt fails); 2 never.
@ http_client_last_proto HttpClient c__h → i@ http_client_last_pq HttpClient c__h → b@ http_client_jar HttpClient c__h → CookieJarA copy of the cookie jar (to inspect); http_client_set_jar replaces it (to seed a session cookie).
@ http_client_set_jar HttpClient c__h sink CookieJar jar → v@ http_client_request HttpClient c__h s method s url sink ( Vec Header ) headers ( Vec u ) body → !HttpResponse HttpClientErrThe one entry point: send method to url with body and the caller's headers, following redirects and carrying cookies. headers is consumed (freed); body is borrowed.
@ 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_body_str HttpResponse r → StringThe response body as a borrowed String view (valid until free).
@ 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.