# `Raxol.Core.Outbound.Response`
[🔗](https://github.com/DROOdotFOO/raxol/blob/v2.8.0/lib/raxol/core/outbound/response.ex#L1)

The bounded response accumulator: a tagged state that starts incomplete.

This is the response half of the outbound policy `Raxol.Core.Outbound`
states, which is why it carries that name. `vet/2` owns rules 1 to 3 (which
target may be reached, and by which addresses) and says outright that rules 4
and 5 belong to whoever owns the request. This module is the part of rule 5 a
request owner cannot write differently per package without the answer
differing too: the size ceiling, the content-length pre-rejection, and the
rule that a read which never terminated has no response at all.

Two packages had a byte-for-byte copy of it, one a submodule of
`Raxol.Web3.Exchange` and one a private map inside
`Raxol.MCP.Client.Transport.Http.Exchange`, because the package graph forbids
either depending on the other. `raxol_core` is what both already depend on,
so this is where the single copy belongs.

## Truncation is never a success

`:state` is `:incomplete` until the terminating response promotes it, and
`to_response/1` accepts nothing else. That is why the accumulator is
a tagged struct rather than a bare map: every halt-based reader in this
ecosystem returns success on abort (`Finch.stream_while/5` answers `{:ok,
acc}` for both a finished response and a `{:halt, acc}`), so an accumulator
whose initial value is shaped like a success turns a refused oversized
response into an empty 200. For a list endpoint that is indistinguishable
from a list with no entries. Here a caller cannot build a response out of an
accumulator the terminating response never reached.

## Trailers are not headers

Mint delivers a chunked trailer section as a SECOND `{:headers, ref, _}`, so
the two are the same tuple and only arrival order tells them apart. They are
collected into `:trailers`, never merged into `:headers`, and never used for
the content-length pre-rejection. A merged trailer is a header the response
never sent, arriving after the point every downstream reader assumed the
header set was closed, and a trailer `content-length` re-runs a size check on
a body that has already been read.

## What it does not own

The ceiling arrives as an argument, so this module holds no policy and has no
defaults of its own. It opens no socket, runs no timer, and calls nothing
from Mint: it pattern-matches Mint's response tuples and never invokes it, so
`raxol_core` gains no dependency. The dial, the wall-clock deadline, the
per-`recv` chunk timeout and each package's error taxonomy stay with the
request owner, because those differ between the two.

The two reasons that can be decided from the response bytes alone are
returned here: `{:too_large, max_bytes}` and `{:transport, reason}`. Both
taxonomies already contain exactly those shapes.

# `headers`

```elixir
@type headers() :: [{String.t(), String.t()}]
```

Header names are lowercase, as Mint delivers them.

# `mint_response`

```elixir
@type mint_response() :: tuple()
```

One of Mint's response tuples: `{:status, ref, status}`,
`{:headers, ref, headers}`, `{:data, ref, binary}`, `{:done, ref}` or
`{:error, ref, reason}`. Matched structurally, never constructed.

# `reason`

```elixir
@type reason() :: {:too_large, non_neg_integer()} | {:transport, term()}
```

# `response`

```elixir
@type response() :: %{
  status: status(),
  headers: headers(),
  trailers: headers(),
  body: binary()
}
```

# `status`

```elixir
@type status() :: non_neg_integer()
```

An HTTP status code.

# `t`

```elixir
@type t() :: %Raxol.Core.Outbound.Response{
  chunks: [binary()],
  headers: headers(),
  headers_seen?: boolean(),
  size: non_neg_integer(),
  state: :incomplete | :complete,
  status: status() | nil,
  trailers: headers()
}
```

`:headers_seen?` is what separates a header block from a trailer block.

Mint delivers trailers as a SECOND `{:headers, ref, _}` and nothing in the
tuple says which one it is, so arrival order is the only evidence there is.

# `absorb`

```elixir
@spec absorb(t(), reference(), [mint_response()], non_neg_integer()) ::
  {:incomplete, t()} | {:complete, t()} | {:error, reason()}
```

Fold Mint responses in, enforcing the size ceiling as they arrive.

Returns `{:incomplete, acc}`, `{:complete, acc}`, or `{:error, reason}`,
where `reason` is already one of the two the response bytes can decide.

Folding stops at the first terminating or refusing response, so responses
after a `{:done, ref}` are not consumed; one request per connection and no
pipelining means there are none.

    iex> alias Raxol.Core.Outbound.Response
    iex> ref = make_ref()
    iex> Response.absorb(Response.new(), ref, [{:data, ref, "0123456789"}], 4)
    {:error, {:too_large, 4}}

    iex> alias Raxol.Core.Outbound.Response
    iex> ref = make_ref()
    iex> headers = [{"content-length", "9000"}]
    iex> Response.absorb(Response.new(), ref, [{:headers, ref, headers}], 1024)
    {:error, {:too_large, 1024}}

# `new`

```elixir
@spec new() :: t()
```

A fresh, incomplete accumulator.

# `to_response`

```elixir
@spec to_response(t()) :: response()
```

The response, or a raise.

Only a `:complete` accumulator has one. A raise rather than an error tuple
because reaching here with an incomplete accumulator is a bug in the read
loop, not an outcome a caller can handle.

---

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