ioxidedocs

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#

HttpClientPoolRingHttpClient
Talks toone origin: a host and a portany http:// or https:// URL
ConnectionsPoolSize of them, opened up front and kept for as long as they workopened as requests need them, up to PoolSize per origin, closed once idle
A request isa method, a byte path, headers and a bodya URL, plus the same method, headers and body
Redirectsreturned as responsesfollowed, as .NET's HttpClient follows them
Built fora reverse proxy; a service that calls one upstream all daycalling 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. .Result or .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; Dispose returns it. Forgetting costs garbage collection, never a stuck connection - the connection went back to the pool when the response was read. After Dispose, the response's memories point at recycled storage.
  • Leave framing to the client. It writes host and, for a body, content-length. Do not add host, content-length or connection yourself.
  • Request heads are checked, not cleaned. A CR, LF or NUL in a method, path or header makes SendAsync throw ArgumentException before 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: close and HTTP/1.0's rules.
  • A sweep on every reactor tick that closes idle connections the origin closed, idle ones past IdleTimeoutMs and old ones past MaxLifetimeMs, 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#