Production reliability layer for Elixir HTTP clients. Adds reliability patterns and enterprise features on top of Finch, Req, or Tesla through an adapter system.
HTTPower supports multiple HTTP clients via adapters — Finch (high-performance, default), Req (batteries-included), and Tesla (bring-your-own-config) — while providing production reliability features that work consistently across all of them:
- Adapter pattern: Choose between Finch, Req, or Tesla HTTP clients
- Middleware pipeline: Rate limiting, circuit breaker, and request deduplication
- Smart retries: Exponential backoff with jitter and Retry-After header support
- PCI-compliant logging: Automatic sanitization of sensitive data with structured metadata
- Telemetry integration: Comprehensive observability for all operations
- Configuration profiles: Pre-built profiles for payment processing, high-volume APIs, and microservices
- Clean error handling: Never raises exceptions, always returns
{:ok, response}or{:error, reason} - Test utilities: Adapter-agnostic test helpers via
HTTPower.Test
Basic Usage
# Simple GET request
HTTPower.get("https://api.example.com/users")
# POST with JSON body
HTTPower.post("https://api.example.com/users",
json: %{name: "Alice", email: "alice@example.com"})
# POST with form data
HTTPower.post("https://api.example.com/login",
form: [username: "alice", password: "secret"])
# POST with raw body
HTTPower.post("https://api.example.com/upload",
body: raw_bytes,
headers: %{"Content-Type" => "application/octet-stream"})
# Skip response decoding
HTTPower.get("https://api.example.com/data", raw: true)
# With configuration options
HTTPower.get("https://api.example.com/slow-endpoint",
timeout: 30,
max_retries: 5,
retry_safe: true
)Test Mode
HTTPower can block real HTTP requests during testing while allowing mocked requests:
# In test configuration
Application.put_env(:httpower, :test_mode, true)
# This will be blocked
HTTPower.get("https://real-api.com") # {:error, %HTTPower.Error{reason: :network_blocked}}
# But this will work with Req.Test
HTTPower.get("https://api.com", plug: {Req.Test, MyApp})Configuration Options
timeout- Request timeout in seconds (default: 60)max_retries- Maximum retries after initial attempt (default: 3)retry_safe- Enable retries for connection resets (default: false)ssl_verify- Enable SSL verification (default: true)proxy- Proxy configuration (default: :system)headers- Request headers mapjson- Data to encode as JSON request body (sets Content-Type and Accept headers)form- Data to encode as form-urlencoded request body (keyword list or map, flat only)params- Query parameters to append to the URL (keyword list or map, flat only)raw- Skip automatic response body decoding when true (default: false)
Return Values
All HTTP methods return either:
{:ok, %HTTPower.Response{}}- an HTTP response was received (any status code){:error, %HTTPower.Error{}}- a transport/network error occurred
Important: {:ok, response} means the server responded, not that the request
"succeeded" in a business logic sense. This includes 4xx and 5xx responses. After
retries are exhausted for retryable status codes (500, 502, 503, 504), the final
server response is still returned as {:ok, response}. Always check
response.status to determine the HTTP outcome:
case HTTPower.get("https://api.example.com/users") do
{:ok, %{status: status}} when status in 200..299 ->
# Success
{:ok, %{status: status} = response} ->
# Server responded with non-2xx (including 5xx after retries exhausted)
{:error, %HTTPower.Error{reason: reason}} ->
# Transport error (timeout, connection refused, etc.)
endHTTPower never raises exceptions for HTTP operations, ensuring your application
stays stable even when external services fail. Configuration errors (such as
passing an unknown profile to new/1) raise ArgumentError at client
construction time, following standard Elixir conventions.
Configured Clients
You can create pre-configured client instances for reuse:
# Create a configured client
client = HTTPower.new(
base_url: "https://api.example.com",
headers: %{"Authorization" => "Bearer token"},
timeout: 30,
max_retries: 5
)
# Use the client for multiple requests
HTTPower.get(client, "/users")
HTTPower.post(client, "/users", json: %{name: "John"})This is especially useful for API clients, different environments, or service-specific configuration.
Summary
Functions
Makes an HTTP DELETE request.
Makes an HTTP GET request.
Makes an HTTP HEAD request.
Creates a new HTTPower client with pre-configured options.
Makes an HTTP OPTIONS request.
Makes an HTTP PATCH request.
Makes an HTTP POST request.
Makes an HTTP PUT request.
Checks if HTTPower is currently in test mode.
Types
Functions
@spec delete( String.t(), keyword() ) :: {:ok, HTTPower.Response.t()} | {:error, HTTPower.Error.t()}
@spec delete(client(), String.t()) :: {:ok, HTTPower.Response.t()} | {:error, HTTPower.Error.t()}
Makes an HTTP DELETE request.
Accepts either a URL string or a configured client as the first argument.
Options
See module documentation for available options.
Examples
# With URL string
HTTPower.delete("https://api.example.com/users/1")
# With configured client
client = HTTPower.new(base_url: "https://api.example.com")
HTTPower.delete(client, "/users/1")
@spec delete(client(), String.t(), keyword()) :: {:ok, HTTPower.Response.t()} | {:error, HTTPower.Error.t()}
@spec get( String.t(), keyword() ) :: {:ok, HTTPower.Response.t()} | {:error, HTTPower.Error.t()}
@spec get(client(), String.t()) :: {:ok, HTTPower.Response.t()} | {:error, HTTPower.Error.t()}
Makes an HTTP GET request.
Accepts either a URL string or a configured client as the first argument.
Options
See module documentation for available options.
Examples
# With URL string
HTTPower.get("https://api.example.com/users")
HTTPower.get("https://api.example.com/users", headers: %{"Authorization" => "Bearer token"})
# With configured client
client = HTTPower.new(base_url: "https://api.example.com")
HTTPower.get(client, "/users")
@spec get(client(), String.t(), keyword()) :: {:ok, HTTPower.Response.t()} | {:error, HTTPower.Error.t()}
@spec head( String.t(), keyword() ) :: {:ok, HTTPower.Response.t()} | {:error, HTTPower.Error.t()}
@spec head(client(), String.t()) :: {:ok, HTTPower.Response.t()} | {:error, HTTPower.Error.t()}
Makes an HTTP HEAD request.
Accepts either a URL string or a configured client as the first argument.
Examples
HTTPower.head("https://api.example.com/users")
client = HTTPower.new(base_url: "https://api.example.com")
HTTPower.head(client, "/users")
@spec head(client(), String.t(), keyword()) :: {:ok, HTTPower.Response.t()} | {:error, HTTPower.Error.t()}
Creates a new HTTPower client with pre-configured options.
Raises ArgumentError if an unknown profile is specified.
Options
base_url- Base URL to prepend to all requestsprofile- Pre-configured profile (:payment_processing,:high_volume_api,:microservices_mesh)- All other options are the same as individual request options (see module documentation)
When using a profile, profile settings are merged with explicit options. Explicit options always take precedence over profile defaults.
Examples
# Simple client with base URL
client = HTTPower.new(base_url: "https://api.example.com")
# Client with authentication and timeouts
client = HTTPower.new(
base_url: "https://api.example.com",
headers: %{"Authorization" => "Bearer token"},
timeout: 30,
max_retries: 5,
retry_safe: true
)
# Use a profile for optimal settings
client = HTTPower.new(
base_url: "https://payment-gateway.com",
profile: :payment_processing
)
# Profile with overrides
client = HTTPower.new(
base_url: "https://api.example.com",
profile: :high_volume_api,
rate_limit: [requests: 2000] # Override profile's rate limit
)
@spec options( String.t(), keyword() ) :: {:ok, HTTPower.Response.t()} | {:error, HTTPower.Error.t()}
@spec options(client(), String.t()) :: {:ok, HTTPower.Response.t()} | {:error, HTTPower.Error.t()}
Makes an HTTP OPTIONS request.
Accepts either a URL string or a configured client as the first argument.
Examples
HTTPower.options("https://api.example.com/users")
client = HTTPower.new(base_url: "https://api.example.com")
HTTPower.options(client, "/users")
@spec options(client(), String.t(), keyword()) :: {:ok, HTTPower.Response.t()} | {:error, HTTPower.Error.t()}
@spec patch( String.t(), keyword() ) :: {:ok, HTTPower.Response.t()} | {:error, HTTPower.Error.t()}
@spec patch(client(), String.t()) :: {:ok, HTTPower.Response.t()} | {:error, HTTPower.Error.t()}
Makes an HTTP PATCH request.
Accepts either a URL string or a configured client as the first argument.
Examples
HTTPower.patch("https://api.example.com/users/1", json: %{name: "Jane"})
client = HTTPower.new(base_url: "https://api.example.com")
HTTPower.patch(client, "/users/1", json: %{name: "Jane"})
@spec patch(client(), String.t(), keyword()) :: {:ok, HTTPower.Response.t()} | {:error, HTTPower.Error.t()}
@spec post( String.t(), keyword() ) :: {:ok, HTTPower.Response.t()} | {:error, HTTPower.Error.t()}
@spec post(client(), String.t()) :: {:ok, HTTPower.Response.t()} | {:error, HTTPower.Error.t()}
Makes an HTTP POST request.
Accepts either a URL string or a configured client as the first argument.
Options
See module documentation for available options. Additionally supports:
json- Data to encode as JSON request bodyform- Data to encode as form-urlencoded request bodybody- Raw request body string
Examples
# With URL string
HTTPower.post("https://api.example.com/users", json: %{name: "John"})
HTTPower.post("https://api.example.com/login",
form: [username: "alice", password: "secret"]
)
# With configured client
client = HTTPower.new(base_url: "https://api.example.com")
HTTPower.post(client, "/users", json: %{name: "John"})
@spec post(client(), String.t(), keyword()) :: {:ok, HTTPower.Response.t()} | {:error, HTTPower.Error.t()}
@spec put( String.t(), keyword() ) :: {:ok, HTTPower.Response.t()} | {:error, HTTPower.Error.t()}
@spec put(client(), String.t()) :: {:ok, HTTPower.Response.t()} | {:error, HTTPower.Error.t()}
Makes an HTTP PUT request.
Accepts either a URL string or a configured client as the first argument.
Options
See module documentation for available options. Additionally supports:
json- Data to encode as JSON request bodyform- Data to encode as form-urlencoded request bodybody- Raw request body string
Examples
# With URL string
HTTPower.put("https://api.example.com/users/1", json: %{name: "John"})
# With configured client
client = HTTPower.new(base_url: "https://api.example.com")
HTTPower.put(client, "/users/1", json: %{name: "John"})
@spec put(client(), String.t(), keyword()) :: {:ok, HTTPower.Response.t()} | {:error, HTTPower.Error.t()}
@spec test_mode?() :: boolean()
Checks if HTTPower is currently in test mode.
In test mode, real HTTP requests are blocked unless they include a :plug option
for mocking with Req.Test.
Examples
Application.put_env(:httpower, :test_mode, true)
HTTPower.test_mode?() # true
Application.put_env(:httpower, :test_mode, false)
HTTPower.test_mode?() # false