# `HTTP.Response`
[🔗](https://github.com/gsmlg-dev/http_fetch/blob/v0.21.17/apps/http_fetch/lib/http/response.ex#L1)

HTTP response struct implementing the Browser Fetch API Response interface.

This module represents an HTTP response with full compatibility with the
Browser Fetch API standard. It supports both buffered and streaming responses,
body consumption tracking, response cloning, and multiple read formats.

## Browser Fetch API Compatibility

This module implements the JavaScript Fetch API Response interface:

- `status` - HTTP status code (e.g., 200, 404, 500)
- `status_text` - Status message ("OK", "Not Found", etc.)
- `ok` - Boolean for success status (200-299)
- `headers` - Response headers as `HTTP.Headers` struct
- `body` - Response body as binary, or stream PID for streaming responses
- `body_used` - Track if body has been consumed
- `url` - The requested URL as `URI` struct
- `redirected` - Whether response was redirected
- `type` - Response type (:basic, :cors, :error, :opaque)
- `stream` - Stream process PID for streaming responses (nil for buffered)

Network responses expose the actual `http_version` (`:http1`, `:http2`, or
`:http3`). HTTP/2 and HTTP/3 retain ordered `{status, headers}` informational
blocks in `informational` when the final response arrives. Buffered trailers
are available in `trailers` when the promise completes, separate from initial
`headers`. Streamed trailers arrive as `{:stream_trailers, stream_pid, headers}`
after body delivery acknowledgements and immediately before `{:stream_end,
stream_pid}`; the immutable response's `trailers` field remains empty.

HTTP/2 retains at most 128 informational blocks and 65,536 bytes of
informational fields in total. Trailers have at most 256 fields and 65,536
bytes. These byte budgets include 32 bytes of overhead per field, and the
protocol's HPACK limits also apply. Exceeding a retention budget fails with
`:http2_informational_limit` or `:http2_trailers_limit` rather than dropping
fields. HTTP/1 informational fields are currently not exposed. HTTP/1 trailers
retain at most 128 fields and 65,536 serialized bytes, including delimiters,
and reject malformed or forbidden fields. Permitted undeclared response
trailers are accepted; `Trailer` declarations are advisory and validated.
Exposing metadata does not establish gRPC support.

## HTTP Content-Encoding

Network responses transparently decode `gzip` (including concatenated members)
and zlib-wrapped `deflate` on HTTP/1.1, HTTP/2 and HTTP/3. Supported coding
chains and repeated header fields decode in reverse order; `identity` is a
no-op. If any coding is unsupported (for example `br`), the entire body passes
through unchanged. No `Accept-Encoding` header is added automatically; callers
may explicitly request `gzip, deflate`.

Decoding applies only to `Content-Encoding`. A `.tgz` download without that
header keeps its original archive bytes, regardless of URL or Content-Type.
Manually constructed responses are also unchanged. HEAD and body-forbidden
statuses expose their empty body without decoding.

All network body consumers (`text/1`, `json/1`, `read_all/1`, `array_buffer/1`,
`blob/1`, `clone/1` and `write_to/2`) see decoded bytes, as do direct stream
readers. Stream decoding is incremental and retains delivery acknowledgements
and trailer ordering. A response stream is consumed once; buffered bodies can
be read repeatedly. Chunk boundaries may change during decoding.

Initial headers retain the server's values, including `Content-Encoding` and
the encoded `Content-Length`. That length also determines whether a response
is buffered or streamed; it is not the decoded byte count. Streaming telemetry
chunk/stop byte counts describe the decoded bytes, while streaming start
retains the encoded length. Decoded bodies can be larger than encoded bodies.

Malformed or truncated supported encodings fail buffered fetches with
`{:error, {:invalid_content_encoding, coding}}`. Streams deliver
`{:stream_error, stream_pid, {:invalid_content_encoding, coding}}`;
`write_to/2` returns that error, while read methods raise the usual stream-read
`RuntimeError`. A failed file write may leave partial decoded bytes. Raw
(non-zlib-wrapped) deflate is rejected as malformed.

## Response Methods

- `json/1` - Parse response as JSON
- `text/1` - Read response as text
- `arrayBuffer/1` - Read response as binary
- `blob/1` - Read response as Blob with metadata
- `clone/1` - Clone response for multiple reads

## Elixir-Specific Differences

**Immutability**: Unlike JavaScript, Elixir responses are immutable. The `body_used`
property won't automatically update across function calls. Use `clone/1` before
multiple reads.

**Synchronous Returns**: Methods like `json()` and `text()` return values directly
instead of Promises, following Elixir conventions.

**Stream Handling**: Large responses expose a stream process in `body`, with
`stream` retained as a compatibility alias.

## Struct Fields

- `status` - HTTP status code (e.g., 200, 404, 500)
- `status_text` - Status message (e.g., "OK", "Not Found")
- `ok` - Boolean indicating success (true for 200-299)
- `headers` - Response headers as `HTTP.Headers` struct
- `body` - Response body as binary, or stream PID for streaming responses
- `body_used` - Whether body has been consumed (Browser API behavior)
- `url` - The requested URL as `URI` struct
- `redirected` - Whether response was redirected
- `type` - Response type (:basic, :cors, :error, :opaque)
- `stream` - Stream process PID for streaming responses (nil for buffered)

## Streaming vs Buffered Responses

Responses are automatically streamed when:

- Content-Length > 5MB
- Content-Length header is missing/unknown

**Buffered responses** have the complete body in the `body` field:

    %HTTP.Response{
      status: 200,
      body: "response data",
      stream: nil
    }

**Streaming responses** expose the stream PID on `body`, with the same PID
retained on `stream` for compatibility:

      %HTTP.Response{
        status: 200,
        body: #PID<0.123.0>,
        stream: #PID<0.123.0>
      }

## Usage

    # Simple text response
    response = HTTP.fetch("https://example.com") |> HTTP.Promise.await()
    text = HTTP.Response.text(response)

    # JSON parsing
    response = HTTP.fetch("https://api.example.com/data") |> HTTP.Promise.await()
    {:ok, json} = HTTP.Response.json(response)

    # Write to file (works with both streaming and buffered)
    response = HTTP.fetch("https://example.com/file.zip") |> HTTP.Promise.await()
    :ok = HTTP.Response.write_to(response, "/tmp/file.zip")

    # Get specific header
    content_type = HTTP.Response.get_header(response, "content-type")

    # Parse Content-Type
    {media_type, params} = HTTP.Response.content_type(response)

## Streaming Responses

For streaming responses, use `read_all/1` or `write_to/2` to consume the stream:

    # Read entire stream into memory
    body = HTTP.Response.read_all(response)

    # Write stream directly to file (more memory efficient)
    :ok = HTTP.Response.write_to(response, "/path/to/file")

# `response_type`

```elixir
@type response_type() :: :basic | :cors | :error | :opaque
```

# `t`

```elixir
@type t() :: %HTTP.Response{
  body: binary() | pid() | nil,
  body_used: boolean(),
  headers: HTTP.Headers.t(),
  http_version: :http1 | :http2 | :http3 | nil,
  informational: [{integer(), HTTP.Headers.t()}],
  ok: boolean(),
  redirected: boolean(),
  response_stream_idle_timeout: pos_integer() | :infinity | nil,
  status: integer(),
  status_text: String.t(),
  stream: pid() | nil,
  trailers: HTTP.Headers.t(),
  type: response_type(),
  url: URI.t() | nil
}
```

# `array_buffer`

```elixir
@spec array_buffer(t()) :: binary()
```

Alias for `arrayBuffer/1` following Elixir naming conventions.

# `arrayBuffer`

```elixir
@spec arrayBuffer(t()) :: binary()
```

Reads the response body as raw binary data (equivalent to JavaScript's ArrayBuffer).

Returns the body as an Elixir binary. For streaming responses, this reads
the entire stream into memory.

## Examples

    iex> response = HTTP.Response.new(status: 200, body: <<1, 2, 3, 4>>)
    iex> HTTP.Response.arrayBuffer(response)
    <<1, 2, 3, 4>>

# `blob`

```elixir
@spec blob(t()) :: HTTP.Blob.t()
```

Reads the response body as a Blob (binary data with metadata).

Returns an `HTTP.Blob` struct containing the body data, MIME type extracted
from the Content-Type header, and size in bytes.

## Examples

    iex> response = HTTP.Response.new(
    ...>   status: 200,
    ...>   body: <<1, 2, 3, 4>>,
    ...>   headers: HTTP.Headers.new([{"content-type", "image/png"}])
    ...> )
    iex> blob = HTTP.Response.blob(response)
    iex> blob.type
    "image/png"
    iex> blob.size
    4

# `clone`

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

Creates a duplicate of the response, allowing the body to be read multiple times.

For buffered responses, this creates a shallow copy with the body duplicated and
`body_used` reset to false.

For streaming responses, this creates a "tee" that splits the stream into two
independent streams that can be consumed separately.

## Examples

    # Clone buffered response
    response = HTTP.Response.new(status: 200, body: "data")
    clone = HTTP.Response.clone(response)

    # Read original
    text1 = HTTP.Response.text(response)

    # Read clone independently
    text2 = HTTP.Response.text(clone)

    # Both contain the same data
    text1 == text2  # true

    # Clone streaming response
    response = HTTP.Response.new(status: 200, stream: stream_pid)
    clone = HTTP.Response.clone(response)

    # Both can be read independently
    data1 = HTTP.Response.read_all(response)
    data2 = HTTP.Response.read_all(clone)

# `content_type`

```elixir
@spec content_type(t()) :: {String.t(), map()}
```

Parses the Content-Type header to extract media type and parameters.

## Examples
    iex> response = %HTTP.Response{headers: HTTP.Headers.new([{"Content-Type", "application/json; charset=utf-8"}])}
    iex> HTTP.Response.content_type(response)
    {"application/json", %{"charset" => "utf-8"}}

    iex> response = %HTTP.Response{headers: HTTP.Headers.new([{"Content-Type", "text/plain"}])}
    iex> HTTP.Response.content_type(response)
    {"text/plain", %{}}

# `get_header`

```elixir
@spec get_header(t(), String.t()) :: String.t() | nil
```

Gets a response header value by name (case-insensitive).

## Examples
    iex> response = %HTTP.Response{headers: HTTP.Headers.new([{"Content-Type", "application/json"}])}
    iex> HTTP.Response.get_header(response, "content-type")
    "application/json"

    iex> response = %HTTP.Response{headers: HTTP.Headers.new([{"Content-Type", "application/json"}])}
    iex> HTTP.Response.get_header(response, "missing")
    nil

# `json`

```elixir
@spec json(t()) :: {:ok, map() | list()} | {:error, term()}
```

Parses the response body as JSON using Elixir's built-in `JSON` module (available in Elixir 1.18+).

Returns:
  - `{:ok, map | list}` if the body is valid JSON.
  - `{:error, reason}` if the body cannot be parsed as JSON.

Note: This method is deprecated in favor of `read_as_json/1` for streaming responses.

# `new`

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

Creates a new Response struct with Browser Fetch API fields populated.

This is the recommended way to create Response structs. It automatically
derives `status_text` and `ok` from the status code, and sets proper defaults
for all Browser API fields.

## Parameters
  - `opts` - Keyword list with fields:
    - `:status` - HTTP status code (default: 0)
    - `:headers` - HTTP.Headers struct (default: empty headers)
    - `:body` - Response body binary or stream PID (default: stream PID or nil)
    - `:url` - Request URL (default: nil)
    - `:stream` - Stream PID for streaming responses (default: nil)
    - `:redirected` - Whether response was redirected (default: false)
    - `:type` - Response type (default: :basic)

The following fields are automatically computed:
  - `status_text` - Derived from status code via HTTP.StatusText
  - `ok` - Set to true if status in 200..299
  - `body_used` - Always initialized to false

## Examples

    iex> response = HTTP.Response.new(status: 200, body: "OK", url: URI.parse("https://example.com"))
    iex> response.status
    200
    iex> response.status_text
    "OK"
    iex> response.ok
    true
    iex> response.body
    "OK"
    iex> response.body_used
    false

    iex> response = HTTP.Response.new(status: 404, headers: HTTP.Headers.new())
    iex> response.status
    404
    iex> response.status_text
    "Not Found"
    iex> response.ok
    false

# `read_all`

```elixir
@spec read_all(t()) :: binary()
```

Reads the entire response body as binary.

For streaming responses, this will consume the entire stream into memory.
For non-streaming responses, returns the existing body.

## Examples
    iex> response = HTTP.Response.new(status: 200, body: "Hello World")
    iex> HTTP.Response.read_all(response)
    "Hello World"

# `read_as_json`

```elixir
@spec read_as_json(t()) :: {:ok, map() | list()} | {:error, term()}
```

Reads the response body and parses it as JSON.

For streaming responses, this will read the entire stream before parsing.

Returns:
  - `{:ok, map | list}` if the body is valid JSON.
  - `{:error, reason}` if the body cannot be parsed as JSON.

## Examples
    iex> response = HTTP.Response.new(status: 200, body: ~s({"key": "value"}))
    iex> HTTP.Response.read_as_json(response)
    {:ok, %{"key" => "value"}}

# `text`

```elixir
@spec text(t()) :: binary()
```

Reads the response body as text.

For streaming responses, this will read the entire stream into memory.

**Note**: Due to Elixir's immutability, the `body_used` field exists for API
compatibility but doesn't prevent multiple reads. Use `clone/1` for clarity
when reading multiple times.

## Examples

    iex> response = HTTP.Response.new(status: 200, body: "Hello")
    iex> HTTP.Response.text(response)
    "Hello"

# `write_to`

```elixir
@spec write_to(t(), String.t()) :: :ok | {:error, term()}
```

Writes the response body to a file.

For streaming responses, this writes chunks directly to the file.
For non-streaming responses, it will write the existing body directly.

## Parameters
  - `response`: The HTTP response to write
  - `file_path`: The path to write the file to

## Returns
  - `:ok` on success
  - `{:error, reason}` on failure

## Examples
    iex> response = %HTTP.Response{body: "file content", stream: nil}
    iex> HTTP.Response.write_to(response, "/tmp/test.txt")
    :ok

---

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