# flare.http.client

> A module of flare_mlake's API.

HTTP/1.1 client with optional TLS support.

Implements a minimal but correct subset of HTTP/1.1 (RFC 7230/7231):
- ``Content-Length``-delimited responses
- ``Transfer-Encoding: chunked`` responses
- ``Connection: close`` (read until EOF) responses
- Up to ``_max_redirects`` automatic 3xx redirects
- Automatic ``Host``, ``User-Agent``, ``Connection`` headers
- Optional ``Authorization`` header via the ``Auth`` trait

Module-level convenience functions (``get``, ``post``, ``put``,
``delete``, ``head``) create a one-shot ``HttpClient`` per call and are
suitable for quick scripts. For multiple requests, prefer instantiating
a shared ``HttpClient``.

``post`` and ``put`` accept a ``String`` body (sets
``Content-Type: application/json`` automatically), a ``json.Value``
body (serialised with ``dumps`` first), or a ``List[UInt8]`` body (sent
as raw bytes with no implicit ``Content-Type``).

Example:
    ```mojo
    from flare.http import get, post, HttpClient, BasicAuth, BearerAuth, Status

    # One-shot GET
    var resp = get("https://httpbin.org/get")

    # One-shot POST JSON — String body sets Content-Type automatically
    var r2 = post("https://httpbin.org/post", '{"k": 1}')

    # Session with base URL and authentication
    with HttpClient("https://httpbin.org", BasicAuth("alice", "s3cr3t")) as c:
        var r = c.get("/basic-auth/alice/s3cr3t")
        r.raise_for_status()
        print(r.text())
    ```

## Structs

### `PoolStats`

```mojo
struct PoolStats
```

A point-in-time view of an ``HttpClient``'s connection pools.

Implements: `Copyable`, `Movable`

Fields:

- `h1_idle: Int`: Idle cleartext HTTP/1.1 connections.
- `tls_idle: Int`: Idle HTTPS HTTP/1.1 connections.
- `quic_idle: Int`: Idle QUIC connections available for HTTP/3.
- `quic_dials: Int`: QUIC connections dialled over this client's lifetime.

### `HttpClient`

```mojo
struct HttpClient
```

A blocking HTTP client (HTTP/1.1, HTTP/2, and HTTP/3).

Opens one connection per request by default; opt into keep-alive
reuse for cleartext HTTP/1.1 with :meth:`with_pool` and into HTTP/3
QUIC connection reuse (on by default once h3 is used). Follows
redirects per a configurable :meth:`with_redirect_policy`, optionally
auto-decompresses responses, retries transient failures via
:meth:`with_retry`, and keeps a session cookie jar via
:meth:`with_cookies`.

This type is ``Movable`` but not ``Copyable``. It supports the context
manager protocol (``__enter__``) for use with ``with``.

Constructors follow a natural ergonomic order:

- ``HttpClient()`` — defaults only
- ``HttpClient("https://api.example.com")`` — base URL positional
- ``HttpClient(BearerAuth("token"))`` — auth first
- ``HttpClient("https://api.example.com", BearerAuth("token"))`` — base URL + auth

Example:
```mojo
# Simple one-liner
var resp = HttpClient().get("https://httpbin.org/get")

# Session with base URL and auth — no repeated prefixes
with HttpClient("https://api.example.com", BearerAuth("tok")) as c:
    c.post("/items", '{"name": "flare"}').raise_for_status()
    var items = c.get("/items").json()
```

Implements: `Movable`

#### `__init__`

```mojo
def __init__(out self, base_url: String = "", max_redirects: Int = Int(10), timeout_ms: Int = Int(30000), user_agent: String = DEFAULT_USER_AGENT, prefer_h2c: Bool = False, h2c_upgrade: Bool = False, prefer_http3: Bool = False, auto_decompress: Bool = True)
```

Initialise an ``HttpClient`` with secure defaults.

Args:

- `base_url`: Optional base URL prepended to relative paths.
- `max_redirects`: Maximum number of redirects to follow (default 10).
- `timeout_ms`: Connect timeout in milliseconds (default 30 s).
    Body reads stay unbounded unless
    :meth:`with_read_timeout` sets a read timeout.
- `user_agent`: Value for the ``User-Agent`` header.
- `prefer_h2c`: When ``True``, ``http://`` requests speak
    HTTP/2 over cleartext via prior knowledge. ``https://``
    requests always negotiate via ALPN regardless.
- `h2c_upgrade`: When ``True``, ``http://`` requests issue an
    HTTP/1.1 ``Upgrade: h2c`` handshake (RFC 7540 §3.2)
    instead of using prior knowledge; the connection switches
    to HTTP/2 only if the server returns ``101 Switching
    Protocols``, otherwise it stays on HTTP/1.1.
- `prefer_http3`: When ``True``, ``https://`` requests prefer
    HTTP/3 over QUIC on first contact rather than waiting
    for an ``Alt-Svc`` advert; any QUIC dial failure falls
    back transparently to h2/h1. Without this, h3 still
    kicks in automatically once a server advertises it via
    ``Alt-Svc``.
- `auto_decompress`: When ``True`` (default), compressed response
    bodies are decoded transparently.

```mojo
def __init__(out self, tls: TlsConfig, base_url: String = "", max_redirects: Int = Int(10), timeout_ms: Int = Int(30000), user_agent: String = DEFAULT_USER_AGENT, prefer_h2c: Bool = False, h2c_upgrade: Bool = False, prefer_http3: Bool = False, auto_decompress: Bool = True)
```

Initialise an ``HttpClient`` with custom TLS configuration.

```mojo
def __init__[A: Auth](out self, auth: A, base_url: String = "", max_redirects: Int = Int(10), timeout_ms: Int = Int(30000), user_agent: String = DEFAULT_USER_AGENT, prefer_h2c: Bool = False, h2c_upgrade: Bool = False, prefer_http3: Bool = False, auto_decompress: Bool = True)
```

Initialise an ``HttpClient`` with authentication.

```mojo
def __init__[A: Auth](out self, base_url: String, auth: A, max_redirects: Int = Int(10), timeout_ms: Int = Int(30000), user_agent: String = DEFAULT_USER_AGENT, prefer_h2c: Bool = False, h2c_upgrade: Bool = False, prefer_http3: Bool = False, auto_decompress: Bool = True)
```

Initialise an ``HttpClient`` with a base URL and authentication.

#### `__deinit__`

```mojo
def __deinit__(deinit self)
```

Free the pooled fds + the ``Alt-Svc`` store owned by this client.

Idempotent on a moved-from client (``_pool._addr == 0`` /
``_alt_svc._addr == 0``). Single-owner: copies are never
produced for ``HttpClient`` because the type is
:trait:`Movable` only, so each heap state is freed exactly
once.

#### `with_pool`

```mojo
def with_pool(var self, max_idle_per_host: Int = Int(8), max_idle_total: Int = Int(64), idle_timeout_ms: Int = Int(90000)) -> Self
```

Enable connection pooling on this client.

Returns the client (move-in / move-out so the call chains
with the regular ``HttpClient(...)`` constructor). Two pools
are enabled, both keyed on the origin: the cleartext HTTP/1.1
pool (``http://``, reuses idle fds) and the HTTPS HTTP/1.1 pool
(``https://``, reuses the whole established ``TlsStream`` so the
TLS handshake is skipped on keep-alive reuse). An ``h2``-
negotiated TLS connection is multiplexed on the h2 path and is
not pooled here.

Example:
```mojo
with HttpClient(base_url="https://api.example.com")
    .with_pool() as c:
    # Two GETs reuse the same TLS connection on idle reuse.
    _ = c.get("/users")
    _ = c.get("/items")
```

Args:

- `max_idle_per_host`: Per-origin idle cap. Default ``8``.
- `max_idle_total`: Total idle cap across origins (cleartext
    pool). Default ``64``. ``0`` disables the total cap.
- `idle_timeout_ms`: Max wallclock age for a pooled connection
    before lazy eviction. Default ``90_000`` ms.

#### `idle_count`

```mojo
def idle_count(self) -> Int
```

Return the total number of connections currently sitting idle across both keep-alive pools (cleartext HTTP/1.1 fds + HTTPS ``TlsStream`` connections). Returns 0 when pooling is disabled.

#### `tls_idle_count`

```mojo
def tls_idle_count(self) -> Int
```

Return the number of idle HTTPS (TLS HTTP/1.1) connections in the pool. Returns 0 when pooling is disabled.

#### `quic_dials`

```mojo
def quic_dials(self) -> Int
```

Number of fresh HTTP/3 (QUIC) connections this client has had to dial (pool misses). Stays at 1 across repeated same-origin h3 requests when reuse is working.

#### `quic_idle_count`

```mojo
def quic_idle_count(self) -> Int
```

Number of established HTTP/3 connections currently idle in the QUIC pool.

#### `with_prefer_http3`

```mojo
def with_prefer_http3(var self, enabled: Bool = True) -> Self
```

Opt this client into HTTP/3 (move-in / move-out so it chains off the constructor like :meth:`with_pool`).

When enabled, ``https://`` requests prefer h3 over QUIC; the
decision still flows through :meth:`http3_wire_choice` and any
QUIC dial failure falls back transparently to the existing
h2/h1 path.

Example:
```mojo
with HttpClient("https://example.com").with_prefer_http3() as c:
    _ = c.get("/")
```

#### `with_h2c`

```mojo
def with_h2c(var self, prior_knowledge: Bool = True, upgrade: Bool = False) -> Self
```

Choose how cleartext HTTP/2 is reached.

Added in v0.11 as the spelling for what the ``prefer_h2c`` and
``h2c_upgrade`` constructor arguments set. Those still work and
are not going away this release, but a knob reachable two ways
is a knob that drifts.

Args:

- `prior_knowledge`: Open with the HTTP/2 preface directly, no
    negotiation. Correct when you already know the origin
    speaks h2c.
- `upgrade`: Offer ``Upgrade: h2c`` on an HTTP/1.1 request and
    switch on a 101. Costs a round trip; works against an
    origin you have not probed. The streaming calls
    (:meth:`get_streaming` and friends) do not take this
    path -- they use prior knowledge when it is set and
    HTTP/1.1 otherwise.

#### `pool_stats`

```mojo
def pool_stats(self) -> PoolStats
```

Idle-connection counts and dial totals across all three pools.

Added in v0.11. Replaces reading ``idle_count`` /
``tls_idle_count`` / ``quic_idle_count`` / ``quic_dials``
separately, which made it easy to check one pool and believe you
had checked them all. Those four accessors still work.

One difference worth knowing when migrating: ``idle_count()``
returns the cleartext and TLS pools *added together*, while
:attr:`PoolStats.h1_idle` is the cleartext pool alone and
:attr:`PoolStats.tls_idle` the TLS one. ``h1_idle + tls_idle``
is the old number.

#### `with_read_timeout`

```mojo
def with_read_timeout(var self, ms: Int) -> Self
```

Bound body reads, not just ``connect(2)`` (move-in / move-out so it chains off the constructor like :meth:`with_pool`).

``timeout_ms`` only ever reached ``connect``; once the
connection was up a read could block forever. That is what a
half-open connection looks like to the client -- the network
drops after the request goes out, the peer never learns it
should FIN, and ``recv`` waits for bytes that will never
arrive. ``ms`` sets ``SO_RCVTIMEO`` on the socket, so a read
that stalls that long raises ``Timeout`` instead.

The bound is per-read inactivity, not a deadline on the whole
response: a large body that keeps arriving never trips it, and
a stalled one trips it after ``ms`` of silence. The client has
no whole-request deadline.

Set it before the first request. The timeout rides on the
socket, so a connection keeps it for its whole lifetime.
Cleartext pooled fds are re-armed on checkout; pooled TLS
connections keep whatever was armed when they were dialled,
and lowering ``ms`` to ``0`` clears neither.

Example:
```mojo
with HttpClient().with_read_timeout(10_000) as c:
    _ = c.get("https://example.com/")
```

Args:

- `ms`: Read timeout in milliseconds. ``0`` (the default)
    disables it and restores the unbounded behaviour.

#### `with_redirect_policy`

```mojo
def with_redirect_policy(var self, policy: RedirectPolicy) -> Self
```

Set the redirect-following policy (move-in / move-out so it chains off the constructor like :meth:`with_pool`).

The default (:meth:`RedirectPolicy.follow_all`) preserves the
legacy unconditional-follow behaviour. Swap in
:meth:`RedirectPolicy.same_origin_only` to refuse cross-origin
redirects (and never leak the Authorization header across
origins), or :meth:`RedirectPolicy.deny` to surface 3xx
responses to the caller unfollowed.

Example:
```mojo
var pol = RedirectPolicy.same_origin_only(max_redirects=5)
with HttpClient("https://api.example.com")
    .with_redirect_policy(pol) as c:
    _ = c.get("/protected")
```

#### `with_retry`

```mojo
def with_retry(var self, policy: RetryPolicy = RetryPolicy()) -> Self
```

Opt into request-level retries on transient failure (move-in / move-out, chains off the constructor).

Retries fire when a request raises (connection/I/O error) or
returns a ``>= 500`` status, up to ``policy.max_attempts``
total attempts. By default only idempotent methods (RFC 9110
sec 9.2.2) are retried; set ``policy.retry_only_idempotent =
False`` to retry any method. Backoff follows the same jittered
exponential schedule as the server-side
:class:`flare.http.reliability.Retry` middleware.

Example:
```mojo
var pol = RetryPolicy()
pol.max_attempts = 4
pol.initial_backoff_ms = 50
with HttpClient().with_retry(pol) as c:
    _ = c.get("https://flaky.example.com/data")
```

#### `with_max_decompressed_bytes`

```mojo
def with_max_decompressed_bytes(var self, n: Int) -> Self
```

Set the decompressed-size ceiling for auto-decompressed response bodies (move-in / move-out, chains off the constructor).

Guards against a decompression bomb: a small compressed body that
inflates without bound. Defaults to 16 MiB; raise or lower it to
fit the largest legitimate response this client expects.

Example:
```mojo
with HttpClient().with_max_decompressed_bytes(64 * 1024 * 1024) as c:
    _ = c.get("https://example.com/big.json.gz")
```

#### `with_proxy`

```mojo
def with_proxy(var self, proxy_url: String) -> Self
```

Route every request through the HTTP proxy at ``proxy_url`` (move-in / move-out, chains off the constructor).

Establishes a ``CONNECT`` tunnel to the origin through the proxy
for both ``http://`` and ``https://`` (TLS then runs over the
tunnel). Overrides the ``HTTP_PROXY`` / ``HTTPS_PROXY`` env
policy; ``NO_PROXY`` is still honored. Pass ``""`` to clear.

Example:
```mojo
with HttpClient().with_proxy("http://127.0.0.1:8080") as c:
    var r = c.get("http://example.com/")
```

#### `with_cookies`

```mojo
def with_cookies(var self) -> Self
```

Enable a per-client cookie jar (move-in / move-out, chains off the constructor).

When enabled, the client captures ``Set-Cookie`` response
headers and replays the stored cookies as a ``Cookie`` request
header on subsequent requests -- the standard session-cookie
flow for a logged-in client. The jar is origin-agnostic (every
stored cookie is sent on every request from this client), which
matches the common single-origin ``HttpClient(base_url=...)``
session.

Example:
```mojo
with HttpClient("https://api.example.com")
    .with_cookies() as c:
    _ = c.post("/login", '{"u":"a","p":"b"}')  # stores session
    _ = c.get("/me")                            # replays cookie
```

#### `cookie_header`

```mojo
def cookie_header(self, url: String) -> String
```

The ``Cookie`` request header value the jar would send to ``url``, or ``""`` when cookies are disabled / none match. Useful for tests and introspection.

#### `record_alt_svc`

```mojo
def record_alt_svc(self, origin: String, alt_svc_header: String)
```

Record an origin's ``Alt-Svc`` response header (RFC 7838) into the discovery cache.

Reads ``self`` (the cache is interior-mutable behind the
:class:`AltSvcStore` handle), so it composes with the
read-``self`` request surface and the transparent auto-record
in :meth:`send`. ``origin`` is the ``host:port`` the response
came from; ``alt_svc_header`` is the raw header value. A later
request to the same origin then sees a fresh h3 advert via
:meth:`http3_wire_choice`. A ``clear`` token evicts the cached
entry. Non-h3 adverts are ignored.

#### `http3_wire_choice`

```mojo
def http3_wire_choice(self, scheme: String, host: String, port: UInt16) -> Int
```

The transparent h3-vs-h2 decision for one request.

Consults the ``prefer_http3`` knob + the per-origin ``Alt-Svc``
cache and returns an
:class:`flare.http._client.alt_svc.Http3WireChoice` codepoint
(``HTTP_3`` or ``HTTP_2_OR_LOWER``). QUIC support is compiled
in, so the only gates are the scheme (``https`` only) and
whether h3 is preferred or freshly advertised. The runtime
send path falls back to h2/h1 on any QUIC dial failure
regardless of this result.

#### `send_chunked`

```mojo
def send_chunked[B: ChunkSource](self, method: String, url: String, mut source: B, content_type: String = "application/octet-stream", body_size: Int = Int(-1), cancel: Cancel = Cancel.never()) -> Response
```

Send a request whose body is streamed from a ``ChunkSource`` using ``Transfer-Encoding: chunked``, without materializing the full body in memory.

For large uploads (multi-MB / multi-GB) this keeps client memory
bounded to one chunk in flight: the source's ``next(cancel)`` is
pulled, framed as a chunked-transfer chunk, and written straight
to the socket. The body is never assembled into a single buffer.

This is an explicit bounded-memory path over HTTP/1.1 (cleartext
or TLS, ALPN forced to ``http/1.1``); it deliberately does not
go through connection pooling, redirects, retries, h2, or h3
(all of which buffer or re-send the body). Use :meth:`post` /
:meth:`send` for those.

Args:

- `method`: HTTP method (e.g. ``"POST"`` / ``"PUT"``).
- `url`: Target URL (absolute or relative to ``base_url``).
- `source`: A ``ChunkSource`` yielding body chunks; pulled to
    exhaustion (``next`` returns ``None``).
- `content_type`: ``Content-Type`` header value. Defaults to
    ``application/octet-stream``.
- `body_size`: Total body length in bytes when known (sent as
    ``Content-Length``); negative selects chunked framing.
- `cancel`: Cancellation token checked while the body is pulled.

Raises: NetworkError: On connection or I/O failure.

#### `__enter__`

```mojo
def __enter__(var self) -> Self
```

Transfer ownership of ``self`` into the ``with`` block.

#### `default`

```mojo
def default() -> Self
```

Return a client with secure defaults (TLS verification enabled).

#### `get`

```mojo
def get(self, url: String) -> Response
```

Perform a GET request.

Args:

- `url`: The URL to request (``http://``, ``https://``, or relative
     path when ``base_url`` is set).

Raises: NetworkError: On connection or I/O failure.
TooManyRedirects: If the redirect limit is exceeded.

#### `get_streaming`

```mojo
def get_streaming(self, url: String) -> HttpStreamResponse
```

Stream a GET response body without buffering it.

Works on ``http://`` and ``https://`` alike. The wire is chosen
the way :meth:`send` chooses it, with one exception: ALPN
settles h2 versus HTTP/1.1 on TLS, and cleartext uses HTTP/1.1
unless :meth:`with_h2c` asked for prior-knowledge h2c. The
``Upgrade: h2c`` handshake is the exception -- a streaming
response cannot be re-read if the origin declines the upgrade,
so this path stays on HTTP/1.1 rather than guessing.

The response head is parsed before this returns; the body is
pulled by :meth:`HttpStreamResponse.read_chunk`, so a
multi-gigabyte download costs one buffer rather than one
allocation the size of the body. The connection is exclusive to
this response and is not pooled.

**Changed in v0.11.** This used to be cleartext-only and raised
on ``https://``, with ``get_streaming_tls`` as its TLS twin
pinned to HTTP/1.1. One entry point covers both now and the
return type changed from ``HttpDownload[TcpStream]`` to
``HttpStreamResponse``.

Args:

- `url`: Absolute URL, or a path when the client has a base URL.

Raises: NetworkError: On a connect, TLS or read failure.
Error: On an unparseable URL.

#### `post_streaming`

```mojo
def post_streaming(self, url: String, body: String) -> HttpStreamResponse
```

POST a JSON body and stream the response.

The shape server-sent events and JSON-lines APIs want: a small
request, then a response read incrementally for as long as the
server keeps writing.

Args:

- `url`: Absolute URL, or a path when the client has a base URL.
- `body`: Request body, sent as ``application/json``.

Raises: NetworkError: On a connect, TLS or read failure.

#### `send_streaming`

```mojo
def send_streaming(self, var req: Request) -> HttpStreamResponse
```

Send ``req`` and stream the response body.

The primitive the other two are written in terms of. Redirects,
retries and cookie handling apply only up to the point the head
is parsed; after that the body belongs to the caller and cannot
be replayed, so nothing re-sends it.

Args:

- `req`: The request (ownership transferred). Framing headers on
    it are ignored -- the writer owns framing.

Raises: NetworkError: On a connect, TLS or read failure.
Error: On an unparseable URL or an unusable header.

#### `post`

```mojo
def post(self, url: String, body: String) -> Response
```

Perform a POST request with a JSON string body.

Sets ``Content-Type: application/json`` automatically. This is the
default for string bodies because virtually every HTTP API that accepts
a string payload expects JSON.

Example:
```mojo
var resp = client.post("https://httpbin.org/post", '{"key": "value"}')
resp.raise_for_status()
```

Args:

- `url`: The target URL (absolute or relative to ``base_url``).
- `body`: The JSON request body as a ``String``.

Raises: NetworkError: On connection or I/O failure.
TooManyRedirects: If the redirect limit is exceeded.

```mojo
def post(self, url: String, body: Value) -> Response
```

Perform a POST request with a ``json.Value`` body.

Serialises ``body`` to JSON with ``dumps`` and sets
``Content-Type: application/json`` automatically.

Args:

- `url`: The target URL (absolute or relative to ``base_url``).
- `body`: A ``json.Value`` to serialise and send.

Raises: NetworkError: On connection or I/O failure.
TooManyRedirects: If the redirect limit is exceeded.

```mojo
def post(self, url: String, body: List[UInt8]) -> Response
```

Perform a POST request with a raw byte body.

No ``Content-Type`` header is set automatically; the caller is
responsible for setting it via a custom ``Request`` if required.

Args:

- `url`: The target URL (absolute or relative to ``base_url``).
- `body`: The raw request body bytes.

Raises: NetworkError: On connection or I/O failure.
TooManyRedirects: If the redirect limit is exceeded.

#### `put`

```mojo
def put(self, url: String, body: String) -> Response
```

Perform a PUT request with a JSON string body.

Sets ``Content-Type: application/json`` automatically.

Args:

- `url`: The target URL (absolute or relative to ``base_url``).
- `body`: The JSON request body as a ``String``.

Raises: NetworkError: On connection or I/O failure.
TooManyRedirects: If the redirect limit is exceeded.

```mojo
def put(self, url: String, body: Value) -> Response
```

Perform a PUT request with a ``json.Value`` body.

Serialises ``body`` to JSON with ``dumps`` and sets
``Content-Type: application/json`` automatically.

Args:

- `url`: The target URL (absolute or relative to ``base_url``).
- `body`: A ``json.Value`` to serialise and send.

Raises: NetworkError: On connection or I/O failure.
TooManyRedirects: If the redirect limit is exceeded.

```mojo
def put(self, url: String, body: List[UInt8]) -> Response
```

Perform a PUT request with a raw byte body.

No ``Content-Type`` header is set automatically.

Args:

- `url`: The target URL (absolute or relative to ``base_url``).
- `body`: The raw request body bytes.

Raises: NetworkError: On connection or I/O failure.
TooManyRedirects: If the redirect limit is exceeded.

#### `delete`

```mojo
def delete(self, url: String) -> Response
```

Perform a DELETE request.

Args:

- `url`: The target URL.

Raises: NetworkError: On connection or I/O failure.
TooManyRedirects: If the redirect limit is exceeded.

#### `head`

```mojo
def head(self, url: String) -> Response
```

Perform a HEAD request.

Identical to ``GET`` but the server MUST NOT include a message body
in the response (RFC 7231 §4.3.2).

Args:

- `url`: The target URL.

Raises: NetworkError: On connection or I/O failure.
TooManyRedirects: If the redirect limit is exceeded.

#### `patch`

```mojo
def patch(self, url: String, body: String) -> Response
```

Perform a PATCH request with a JSON string body.

Sets ``Content-Type: application/json`` automatically.

Args:

- `url`: The target URL (absolute or relative to ``base_url``).
- `body`: The JSON request body as a ``String``.

Raises: NetworkError: On connection or I/O failure.
TooManyRedirects: If the redirect limit is exceeded.

```mojo
def patch(self, url: String, body: Value) -> Response
```

Perform a PATCH request with a ``json.Value`` body.

Serialises ``body`` to JSON with ``dumps`` and sets
``Content-Type: application/json`` automatically.

Args:

- `url`: The target URL (absolute or relative to ``base_url``).
- `body`: A ``json.Value`` to serialise and send.

Raises: NetworkError: On connection or I/O failure.
TooManyRedirects: If the redirect limit is exceeded.

```mojo
def patch(self, url: String, body: List[UInt8]) -> Response
```

Perform a PATCH request with a raw byte body.

No ``Content-Type`` header is set automatically.

Args:

- `url`: The target URL (absolute or relative to ``base_url``).
- `body`: The raw request body bytes.

Raises: NetworkError: On connection or I/O failure.
TooManyRedirects: If the redirect limit is exceeded.

#### `send`

```mojo
def send(self, req: Request) -> Response
```

Send an HTTP request and return the response.

Follows redirects per the configured :class:`RedirectPolicy`
(default: follow all, capped at ``max_redirects``), transparently
decodes a compressed body when ``auto_decompress`` is on, and
replays / captures cookies when the jar is enabled. When
:meth:`with_retry` opted in, the whole exchange is retried on a
transient failure per the :class:`RetryPolicy`.

Args:

- `req`: The request to send.

Raises: NetworkError: On I/O failure.
TooManyRedirects: If the redirect cap is exceeded.
Error: On a cross-origin redirect refused by a
``same_origin_only`` policy.

## Aliases

### `FLARE_VERSION`

```mojo
comptime FLARE_VERSION = String("0.13.0")
```

The library version, as reported in the default ``User-Agent``.

Bump alongside ``pixi.toml`` and ``recipe.yaml`` at release.

### `DEFAULT_USER_AGENT`

```mojo
comptime DEFAULT_USER_AGENT = (String("flare/") + StringSpan(FLARE_VERSION))
```

Default ``User-Agent`` sent when the caller does not set one.

Was the literal ``"flare/0.1.0"`` through v0.10, which had been wrong
for nine releases because nothing derived it from the version.
