# `Raxol.MCP.Client.Transport.Http.Exchange`
[🔗](https://github.com/DROOdotFOO/raxol/blob/v2.8.0/lib/raxol/mcp/client/transport/http/exchange.ex#L2)

The pinned dial and the bounded read: one request, one bounded response.

ADR-0037 decision 5, which says explicitly that this transport takes the
shape ADR-0038 decision 3 and 4 already implement for the REST side
(`packages/raxol_web3/lib/raxol/web3/dial.ex`,
`packages/raxol_web3/lib/raxol/web3/exchange.ex`). One design in two
packages rather than two: copying is forced by the package graph, since
`raxol_web3` depends on `raxol_mcp` and not the other way round.

## The checked address is the address dialled

`run/3` takes the `:inet` address tuples `Raxol.Core.Outbound.vet/2`
checked, never a name, so there is no code path here that could consult a
resolver and no window for a record that changes between the check and the
connect. A binary where an address belongs is refused as
`{:not_an_address, value}` rather than resolved.

Mint derives everything that must follow the NAME from one `:hostname`
option: SNI, the certificate match function, and the HTTP/1 `Host` header.
So `:server_name_indication` and `:customize_hostname_check` are never set
by hand -- setting either replaces a value Mint already derived correctly
-- and `:verify` and `:verify_fun` are refused outright, because a silently
weakened handshake is the failure this module exists to prevent. A
caller-supplied `:transport_opts` never reaches here: the only caller is
`Raxol.MCP.Client.Transport.Http`, which does not accept one.

There is no connection pool. A pool key is `{scheme, host, port, tag}`, so
a vetted IP in the host position makes every hostname that resolves to that
address share one pool and inherit whichever identity opened it first.

## The bounded read

The send, the four bounds (size ceiling, content-length pre-rejection,
wall-clock deadline, per-`recv` silence) and the close are
`Raxol.MCP.BoundedExchange`, which `Raxol.Web3.Exchange` reads through too:
the two used to be identical copies, down to the read-phase taxonomy. The
reasoning for each bound is in that module. What stays here is the dial and
the connect-side reasons, which are this transport's alone.

## Truncation is never a success

The accumulator is `Raxol.Core.Outbound.Response`: it starts `:incomplete`
and only the terminating response promotes it, so `run/3` cannot return
`{:ok, _}` for a partial response by construction. That matters more here
than for a REST body: a truncated SSE stream still contains whole frames,
so a reader that returned what it had would hand the session a
plausible-looking response to a request that never completed.

## Redirects

A 3xx is returned as a response, not followed, and the caller refuses it.
Following one would replay this transport's `Authorization` header at an
origin the upstream chose.

# `reason`

```elixir
@type reason() ::
  :no_addresses
  | {:not_an_address, term()}
  | {:forbidden_transport_opts, [atom()]}
  | {:too_large, pos_integer()}
  | {:timeout, :connect | :chunk | :deadline}
  | {:transport, term()}
```

# `request`

```elixir
@type request() :: %{
  :method =&gt; String.t(),
  :path =&gt; String.t(),
  optional(:headers) =&gt; Mint.Types.headers(),
  optional(:body) =&gt; iodata() | nil
}
```

# `response`

```elixir
@type response() :: Raxol.MCP.BoundedExchange.response()
```

# `vetted`

```elixir
@type vetted() :: %{
  uri: URI.t(),
  addresses: [:inet.ip_address()],
  hostname: String.t()
}
```

# `connect`

```elixir
@spec connect(vetted(), keyword()) :: {:ok, Mint.HTTP.t()} | {:error, reason()}
```

Connect to the first reachable vetted address, presenting `hostname` as the
identity.

Every address is tried in turn, which is the failover `:gen_tcp` performs
for a name and which pinning would otherwise lose. Public so that the dial
can be tested against a real local TLS listener, which the reject set
refuses to let the guarded path reach.

`:deadline_at` (a monotonic millisecond stamp, set by `run/3`) clamps
every attempt: N addresses at `connect_timeout_ms` each would otherwise
exceed the whole exchange's budget before a byte was sent.

# `run`

```elixir
@spec run(vetted(), request(), keyword()) :: {:ok, response()} | {:error, reason()}
```

Dial a vetted target and read one bounded response.

Options: `:port` (defaults to the vetted URI's port), `:connect_timeout_ms`,
`:deadline_ms`, `:chunk_timeout_ms`, `:max_bytes`, and `:transport_opts`
for a test's own trust store. The connection is closed on every path.

The deadline is computed HERE, before the dial, and what reaches
`BoundedExchange.run/3` is what is left of it. `BoundedExchange`'s
moduledoc says the deadline is taken at entry so the send counts against
it; starting that clock after the connect made the real wall bound
`connect_timeout_ms x addresses + deadline_ms + chunk_timeout_ms`, which
was stated nowhere and, at the defaults, exceeded the client's 30 s
`call_timeout`. Each connect attempt is clamped to the remaining budget
for the same reason, so the bound is `deadline_ms` plus at most one
`chunk_timeout_ms` of read overshoot.

---

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