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 asHTTP.Headersstructbody- Response body as binary, or stream PID for streaming responsesbody_used- Track if body has been consumedurl- The requested URL asURIstructredirected- Whether response was redirectedtype- 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 JSONtext/1- Read response as textarrayBuffer/1- Read response as binaryblob/1- Read response as Blob with metadataclone/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 asHTTP.Headersstructbody- Response body as binary, or stream PID for streaming responsesbody_used- Whether body has been consumed (Browser API behavior)url- The requested URL asURIstructredirected- Whether response was redirectedtype- 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")
Summary
Functions
Alias for arrayBuffer/1 following Elixir naming conventions.
Reads the response body as raw binary data (equivalent to JavaScript's ArrayBuffer).
Reads the response body as a Blob (binary data with metadata).
Creates a duplicate of the response, allowing the body to be read multiple times.
Parses the Content-Type header to extract media type and parameters.
Gets a response header value by name (case-insensitive).
Parses the response body as JSON using Elixir's built-in JSON module (available in Elixir 1.18+).
Creates a new Response struct with Browser Fetch API fields populated.
Reads the entire response body as binary.
Reads the response body and parses it as JSON.
Reads the response body as text.
Writes the response body to a file.
Types
@type response_type() :: :basic | :cors | :error | :opaque
@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 }
Functions
Alias for arrayBuffer/1 following Elixir naming conventions.
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>>
@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
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)
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", %{}}
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
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.
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.StatusTextok- Set to true if status in 200..299body_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
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"
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"}}
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"
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 writefile_path: The path to write the file to
Returns
:okon 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