HTTPower.Adapter behaviour (HTTPower v0.24.0)

Copy Markdown View Source

Behaviour for HTTPower adapters.

Adapters are responsible for making the actual HTTP requests using a specific HTTP client library (Req, Tesla, etc.). HTTPower's features like retry logic, circuit breakers, and rate limiting sit above the adapter layer.

Implementing an Adapter

To implement an adapter, create a module that implements the request/5 callback:

defmodule MyApp.CustomAdapter do
  @behaviour HTTPower.Adapter

  @impl true
  def request(method, url, body, headers, opts) do
    # Make HTTP request using your preferred HTTP client
    # Convert response to {:ok, %HTTPower.Response{}} or {:error, reason}
  end
end

Built-in Adapters

HTTPower ships with three built-in adapters:

Using an Adapter

Specify the adapter when making requests:

# Use default (Finch) adapter
HTTPower.get("https://api.example.com")

# Use explicit Req adapter
HTTPower.get("https://api.example.com", adapter: HTTPower.Adapter.Req)

# Use Tesla adapter with your Tesla client
tesla_client = Tesla.client([Tesla.Middleware.JSON])
HTTPower.get("https://api.example.com",
  adapter: {HTTPower.Adapter.Tesla, tesla_client})

Adapter Contract

Adapters must return responses in a standardized format:

  • Success: {:ok, %HTTPower.Response{status: integer(), headers: %{String.t() => [String.t()]}, body: binary() | nil}}. Headers are always list-valued (see normalize_response_headers/1); the body is the raw, undecoded payload — HTTPower.Codec decodes it above the adapter layer.
  • Failure: {:error, reason} where reason is a bare atom (transport failures via unwrap_transport_error/1; usage errors like :missing_tesla_client). HTTPower wraps it into an %HTTPower.Error{} at the retry boundary (HTTPower.Retry), so the adapter never builds the error struct itself.

HTTPower's retry logic and error handling work consistently across all adapters.

Connection Options (Uniform Vocabulary)

HTTPower keeps a uniform connection vocabulary — timeout:, ssl_verify:, proxy: — so callers write HTTPower's terms, never a backend's dialect. These options arrive in opts untouched, and the adapter maps them onto its backend wherever the backend accepts them:

  • Req honors all three per request.
  • Finch honors timeout: (and pool_timeout:) per request; TLS and proxy are fixed at pool start (config :httpower, :finch_pools), so per-request ssl_verify:/proxy: have no effect.
  • Tesla defers all three to the user's Tesla client.

This bounded leak is the accepted contract: an adapter that cannot honor an option per request must say so in its moduledoc and point at where its backend fixes the setting — never silently approximate it. Custom adapters should follow the same rule. timeout: is expressed in seconds; the adapter converts to its backend's unit.

Summary

Callbacks

Makes an HTTP request using the adapter's underlying HTTP client.

Functions

Normalizes response headers into a %{String.t() => [String.t()]} map.

Prepares request headers, ensuring a valid map is returned.

Maps a rescued exception to a bare-reason transport error, or reraises it.

Extracts the bare reason from a backend transport/protocol error struct.

Callbacks

request(method, url, body, headers, opts)

@callback request(
  method :: atom(),
  url :: URI.t(),
  body :: term(),
  headers :: map(),
  opts :: keyword()
) :: {:ok, HTTPower.Response.t()} | {:error, term()}

Makes an HTTP request using the adapter's underlying HTTP client.

Parameters

  • method - HTTP method as an atom (:get, :post, :put, :delete)
  • url - Full URL as a URI struct
  • body - Request body, already encoded by HTTPower.Codec (binary or nil)
  • headers - Map of request headers
  • opts - Keyword list of adapter-specific options

Returns

  • {:ok, %HTTPower.Response{}} on successful request
  • {:error, reason} (bare atom) on failure

Examples

@impl true
def request(:get, "https://api.example.com/users", nil, %{}, _opts) do
  {:ok, %HTTPower.Response{
    status: 200,
    headers: %{"content-type" => ["application/json"]},
    body: ~s({"users":[]})
  }}
end

Functions

normalize_response_headers(headers)

@spec normalize_response_headers([{term(), term()}] | map()) :: %{
  required(String.t()) => [String.t()]
}

Normalizes response headers into a %{String.t() => [String.t()]} map.

HTTP headers can repeat (e.g. Set-Cookie), so values are always lists. Accepts either a list of {key, value} tuples (as Finch, Tesla, and Req < 0.5 return) or a map whose values are strings or lists (as Req >= 0.5 returns), producing a single consistent shape across all adapters. Keys are downcased to strings; tuple order is preserved for repeated keys.

prepare_headers(headers)

@spec prepare_headers(map() | nil) :: map()

Prepares request headers, ensuring a valid map is returned.

Used by all adapters and the test interceptor to ensure consistent header handling across the library.

rescue_transport_error(exception, stacktrace)

@spec rescue_transport_error(Exception.t(), Exception.stacktrace()) ::
  {:error, term()}

Maps a rescued exception to a bare-reason transport error, or reraises it.

Adapters wrap their HTTP call in a rescue to honor the never-raises contract, but only genuine transport failures should be swallowed. A backend transport-error struct is normalized to its bare :reason (so HTTPower.Retry can classify it); anything else — an ArgumentError from a bad option, a bug in HTTPower's own code — is reraised with its original stacktrace so it surfaces instead of masquerading as a retryable {:error, _}.

unwrap_transport_error(error)

@spec unwrap_transport_error(term()) :: term()

Extracts the bare reason from a backend transport/protocol error struct.

The backends wrap transport and HTTP-protocol failures in their own structs (Finch.TransportError/Finch.HTTPError/Finch.Error, Mint.TransportError/ Mint.HTTPError, Req.TransportError/Req.HTTPError), each carrying the underlying reason in :reason. Unwrapping to the bare reason — an atom, or a tuple for HTTP/2 stream errors like {:server_closed_request, :refused_stream} — lets HTTPower.Retry classify the failure consistently across adapters.

Backend dialect synonyms are folded onto HTTPower's canonical reason atoms (e.g. Finch's :request_timeout:timeout, :connection_closed:closed) so the same failure yields the same reason regardless of adapter. Reasons with no exact canonical equivalent pass through unchanged.

Matched structurally on __struct__ so this module carries no compile-time dependency on the optional backends. Any non-matching value is returned unchanged.