# `Snodo.Client.HTTP`
[🔗](https://github.com/joshrotenberg/snodo/blob/v0.3.0/lib/snodo/client/http.ex#L1)

Streamable HTTP transport for `Snodo.Client.connect({:http, url}, opts)`.

Each request is one HTTP/1.1 `POST` on its own `:gen_tcp` or `:ssl`
connection, closed before the request returns, so no connection or process
outlives it. The request headers come from the protocol dialect's
`transport_policy/1`, the same declaration the server admits requests
against: the accepted and request media types, and every mirrored header
(for `2026-07-28`, `MCP-Protocol-Version`, `Mcp-Method`, and `Mcp-Name`) read
from the request body. A mirrored value that is not plain printable ASCII is
sent in the `=?base64?...?=` form when the policy allows it. A header value
that contains CR, LF, or NUL is refused with a -32000 transport error.

The response may be `application/json` or `text/event-stream`. From an event
stream the transport returns the response whose ID matches the request and
drops notifications such as progress. A JSON-RPC error body is returned
whatever the HTTP status, so `Snodo.Client` decodes it as `{:error,
%Snodo.Error{}}`. Anything else is a -32000 transport error with the status and
body in `cause`. A timeout closes the connection, which the server treats as
cancellation.

The response is read by this module rather than `:httpc`, which reads the
body of any status other than 200 and 206 in full before returning it.
`:max_response_bytes` is checked as the response arrives, whatever its
status: a `Content-Length` over the limit is refused before the body is read,
a chunked body is refused at the first chunk that would pass it, and a body
that ends when the connection closes is refused at the read that passes it.
An event stream is one body, so the limit applies to the whole stream,
notifications included. The status line and headers are held to the same
limit. Over the limit the connection is closed and the request returns a
-32000 transport error with `cause: {:max_response_bytes, limit}`.

Options:

  * `:headers` - extra request headers as `{name, value}` string pairs, for
    example `[{"authorization", "Bearer " <> token}]`. The transport owns
    `host`, `content-type`, `content-length`, `transfer-encoding`, and
    `connection`, and refuses them here.
  * `:ssl` - `:ssl` client options for `https` URLs. The default verifies the
    peer against `:public_key.cacerts_get/0` and checks the host name.
  * `:connect_timeout` - milliseconds to establish the connection. Defaults
    to the request timeout.
  * `:max_response_bytes` - the largest response to accept, default 16 MiB,
    the stdio client's line limit.

# `state`

```elixir
@type state() :: %{
  scheme: String.t(),
  host: charlist(),
  port: :inet.port_number(),
  authority: String.t(),
  target: String.t(),
  socket_options: [:gen_tcp.connect_option()],
  headers: [{String.t(), String.t()}],
  ssl: keyword(),
  connect_timeout: timeout() | nil,
  max_response_bytes: pos_integer()
}
```

# `encode_sentinel`

```elixir
@spec encode_sentinel(String.t()) :: String.t()
```

Encodes a mirrored header value with the base64 sentinel when it is not
plain printable ASCII, or when the plain value could be mistaken for one.

---

*Consult [api-reference.md](api-reference.md) for complete listing*
