HTTP client Get started
ioxide.httpclient
The HTTP/1.1 client for ioxide. Its connections are opened, written and read on the reactor's own io_uring ring, and every response resumes the awaiting handler inline on that reactor - so a request never crosses a thread on its way out or back.
dotnet add package ioxide.httpclient
It depends on ioxide core alone. https uses the BoringSSL that ships in the core package, loaded only when a TLS context is created.
Two ways to use it#
| HttpClientPool | RingHttpClient | |
|---|---|---|
| Talks to | one origin: a host and a port | any http:// or https:// URL |
| Connections | PoolSize of them, opened up front and kept for as long as they work | opened as requests need them, up to PoolSize per origin, closed once idle |
| A request is | a method, a byte path, headers and a body | a URL, plus the same method, headers and body |
| Redirects | returned as responses | followed, as .NET's HttpClient follows them |
| Built for | a reverse proxy; a service that calls one upstream all day | calling APIs and sites by URL |
Both are created once per reactor, from its OnStart, and used from that reactor's handlers. RingHttpClient keeps an HttpClientPool per origin inside, so everything below about pools applies to both.
Ready to write code? The quick start has a complete program for each: a server whose handlers call one upstream, and a program that fetches any URL.
Rules that keep it fast and correct#
- One per reactor, made in
OnStart. A pool's connections belong to the ring of the reactor that opened them. Use it only from that reactor's handlers - it is not thread-safe, by design, and it never needs to be. - Await; never block.
.Resultor.Wait()on the reactor thread waits for a completion that only that thread can run. - Dispose every response. A response owns a pooled buffer holding its headers and body;
Disposereturns it. Forgetting costs garbage collection, never a stuck connection - the connection went back to the pool when the response was read. AfterDispose, the response's memories point at recycled storage. - Leave framing to the client. It writes
hostand, for a body,content-length. Do not addhost,content-lengthorconnectionyourself. - Request heads are checked, not cleaned. A CR, LF or NUL in a method, path or header makes
SendAsyncthrowArgumentExceptionbefore a connection is taken. A proxy forwarding what it was sent should check it first and answer 400 itself, as the reverse proxy recipe does.
What it does#
- Names resolved when a connection opens, IPv4 and IPv6, each address tried in turn, a connect timeout per address.
- https with the certificate checked against the host or a name you set, the system trust store or your own CA, ALPN and client certificates.
- Keep-alive pooling that reuses the most recently used connection, honours
Connection: closeand HTTP/1.0's rules. - A sweep on every reactor tick that closes idle connections the origin closed, idle ones past
IdleTimeoutMsand old ones pastMaxLifetimeMs, and opens their replacements - at no cost to a request. - Waiting for a busy pool without a timer or a thread: a bounded acquire, parallel opens for a burst, and a backoff when the origin refuses.
- Responses framed by Content-Length, chunked encoding or the close, interim 1xx responses skipped, a size ceiling, and a hardened parser that refuses ambiguous framing.
- RingHttpClient: a pool per origin opened on first use and dropped when idle, one TLS context for every https origin, and .NET's redirect rules.
Where to go next#
Quick start
A complete program for each way to use it, ready to run.
Features
DNS, IPv6, TLS, timeouts, idle and lifetime limits, framing, redirects and errors in detail.
Recipes
A reverse proxy, several backends, JSON APIs, private CAs, mutual TLS, an origin that moves.
How it works
One reactor's pool in 3D, a connection opening, the sweep, and what each part costs.
API reference
Every type, member and option, with defaults.
Limits
What it does not do yet.