HTTP.Response (http_fetch v0.8.0)

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 (nil 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)

Response Methods

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 use Elixir processes for streaming instead of ReadableStream.

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 (nil 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 have body: nil and a stream PID:

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

Usage

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

# JSON parsing
{:ok, 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)
{:ok, 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

response_type()

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

t()

@type t() :: %HTTP.Response{
  body: binary() | nil,
  body_used: boolean(),
  headers: HTTP.Headers.t(),
  ok: boolean(),
  redirected: boolean(),
  status: integer(),
  status_text: String.t(),
  stream: pid() | nil,
  type: response_type(),
  url: URI.t() | nil
}

Functions

array_buffer(response)

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

Alias for arrayBuffer/1 following Elixir naming conventions.

arrayBuffer(response)

@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(response)

@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(response)

@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(response)

@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(response, name)

@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(response)

@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(opts \\ [])

@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 (default: 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(response)

@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(response)

@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(response)

@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(response, file_path)

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

Writes the response body to a file.

For streaming responses, this will read the entire stream and write it 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