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 ) ( http_response_free r ) } F e → { ( nurl_eprintln ( http_client_err_name e ) ) } } ( http_client_free c ) ^ 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 ) ( http_response_free r ) } F e → { ( nurl_eprintln ( http_client_err_name e ) ) } } ( http_client_free c )
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 a heap *HttpClient; free it with http_client_free, which closes every pooled connection. A returned HttpResponse is owned by the caller (http_response_free).
: | 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: HcOrigin: HcOrigin {
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 H3Client, beside whatever TCP connection the origin still holds.
: HttpClient: HttpClient {
CookieJar jar
( Vec i ) origins // *HcOrigin boxed, so the structs have 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 ────────────────────────────────────────────────────────────
@ http_client_new → *HttpClient@ http_client_set_verify * HttpClient c 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 b on → vFollow 3xx redirects (default) or hand the 3xx response back.
@ http_client_set_max_redirects * HttpClient c i n → v@ http_client_set_timeout * HttpClient c i ms → vPer read/write deadline in milliseconds (0 = none). A stalled server answers HcTimeout instead of hanging.
@ http_client_set_decompress * HttpClient c 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 i n → vCap the (decoded) response body; a larger body answers HcTooLarge.
@ http_client_set_user_agent * HttpClient c s ua → v@ http_client_set_h3 * HttpClient c 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 → i@ http_client_last_pq * HttpClient c → b@ http_client_jar * HttpClient c → CookieJarDirect access to the cookie jar (seed a session cookie, inspect, …).
@ http_client_request * HttpClient c s method s url ( 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 * HttpClient c → v