# `HTTPower`
[🔗](https://github.com/mdepolli/httpower/blob/v0.22.0/lib/httpower.ex#L1)

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 map
- `json` - 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.)
    end

HTTPower 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.

# `client`

```elixir
@type client() :: %HTTPower{base_url: String.t() | nil, options: keyword()}
```

# `delete`

```elixir
@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")

# `delete`

```elixir
@spec delete(client(), String.t(), keyword()) ::
  {:ok, HTTPower.Response.t()} | {:error, HTTPower.Error.t()}
```

# `get`

```elixir
@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")

# `get`

```elixir
@spec get(client(), String.t(), keyword()) ::
  {:ok, HTTPower.Response.t()} | {:error, HTTPower.Error.t()}
```

# `head`

```elixir
@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")

# `head`

```elixir
@spec head(client(), String.t(), keyword()) ::
  {:ok, HTTPower.Response.t()} | {:error, HTTPower.Error.t()}
```

# `new`

```elixir
@spec new(keyword()) :: client()
```

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 requests
- `profile` - 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
    )

# `options`

```elixir
@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")

# `options`

```elixir
@spec options(client(), String.t(), keyword()) ::
  {:ok, HTTPower.Response.t()} | {:error, HTTPower.Error.t()}
```

# `patch`

```elixir
@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"})

# `patch`

```elixir
@spec patch(client(), String.t(), keyword()) ::
  {:ok, HTTPower.Response.t()} | {:error, HTTPower.Error.t()}
```

# `post`

```elixir
@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 body
- `form` - Data to encode as form-urlencoded request body
- `body` - 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"})

# `post`

```elixir
@spec post(client(), String.t(), keyword()) ::
  {:ok, HTTPower.Response.t()} | {:error, HTTPower.Error.t()}
```

# `put`

```elixir
@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 body
- `form` - Data to encode as form-urlencoded request body
- `body` - 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"})

# `put`

```elixir
@spec put(client(), String.t(), keyword()) ::
  {:ok, HTTPower.Response.t()} | {:error, HTTPower.Error.t()}
```

# `test_mode?`

```elixir
@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

---

*Consult [api-reference.md](api-reference.md) for complete listing*
