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
endBuilt-in Adapters
HTTPower ships with three built-in adapters:
HTTPower.Adapter.Finch- Uses Finch HTTP client, built on Mint + NimblePool (default)HTTPower.Adapter.Req- Uses Req HTTP client (batteries-included)HTTPower.Adapter.Tesla- Uses Tesla HTTP client (bring-your-own-config)
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 (seenormalize_response_headers/1); the body is the raw, undecoded payload —HTTPower.Codecdecodes it above the adapter layer. - Failure:
{:error, reason}wherereasonis a bare atom (transport failures viaunwrap_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:(andpool_timeout:) per request; TLS and proxy are fixed at pool start (config :httpower, :finch_pools), so per-requestssl_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
@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 structbody- Request body, already encoded byHTTPower.Codec(binary or nil)headers- Map of request headersopts- 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
@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.
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.
@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, _}.
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.