Skip to main content

CHANGELOG.md

# Changelog

All notable changes to this project will be documented in this file.

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [0.23.0] - 2026-06-28

### Added

- **`HTTPower.Sanitizer` module** — Extracted the PCI-compliant header/body sanitization logic out of `HTTPower.Logger` into a neutral, dedicated module. It is now the single source of truth, used by both `HTTPower.Client` (for telemetry metadata) and `HTTPower.Logger` (for log output). `HTTPower.Logger.sanitize_headers/1` and `HTTPower.Logger.sanitize_body/1` remain available as delegations for backward compatibility.

- **`pool_timeout` option for the Finch adapter** — Sets the maximum time (ms) to wait when checking out a pooled connection. Previously unconfigurable, so it fell back to Finch's hardcoded 5000ms default; that default still applies when the option is not set.

- **SSRF guardrails: `:block_private_ips` and `:allowed_hosts` options** — Opt-in egress controls validated before any HTTP call. `block_private_ips: true` rejects requests whose host is a loopback, RFC1918 private, link-local (incl. the `169.254.169.254` cloud-metadata address), or IPv6 unique-local/loopback literal, plus the `localhost` hostname, with `{:error, %HTTPower.Error{reason: :blocked_private_ip}}`. `allowed_hosts: [...]` restricts requests to an explicit host allowlist (case-insensitive), returning `{:error, %HTTPower.Error{reason: :host_not_allowed}}` otherwise. Both default off and can be set per request or via `config :httpower`. Host checks are **literal only** (no DNS resolution), so a hostname that resolves to a private IP is not caught — pair the two options when a strict boundary is required.

### Changed

- **BREAKING: `HTTPower.Retry.execute_with_retry/6` is now `execute_with_retry/2`** (with an optional third `telemetry_metadata` argument). It now takes a 0-arity `execute_fn` closure plus the request `opts`, instead of `(method, url, body, headers, adapter, opts)`. This breaks the circular dependency between `HTTPower.Retry` and `HTTPower.Client` — Retry no longer references Client, has no knowledge of adapters, and is independently testable. Retry behavior is unchanged; this only affects code that called this internal execution wrapper directly. (`HTTPower.Client.call_adapter/6`, previously `@doc false`, is now private.)

- **BREAKING: `HTTPower.Adapter.prepare_headers/2` is now `prepare_headers/1`** — the `method` argument was never used. Affects only custom adapter authors who called this public helper directly.

- **Finch adapter: removed the non-functional per-request `ssl_verify`/`proxy` handling** — these options never took effect with the Finch adapter and were silently discarded: Finch configures TLS and proxy at the **pool** level (baked in at `Finch.start_link`), and `Finch.request/3` has no per-request connection options. The dead option-building (and the misleading tests that asserted it) is removed, so runtime behavior is unchanged. TLS/proxy are now documented as pool-level via `config :httpower, :finch_pools` (which flows into Finch's `:pools`). Without explicit TLS config the pool inherits Mint's default `verify: :verify_peer`, so certificates are still verified by default. The Req adapter continues to honor `ssl_verify`/`proxy` per request (it starts a Finch pool per distinct `connect_options`); the Tesla adapter configures them on the Tesla client.

- **BREAKING: Minimum Elixir version raised to 1.15** (was 1.14). HTTPower declares an unbounded optional `finch` dependency (`>= 0.19.0`), but Finch 0.22.0+ requires Elixir `~> 1.15`. The previous `~> 1.14` floor was a trap: a fresh consumer on Elixir 1.14 would resolve a current Finch and fail to compile, while HTTPower's own CI only passed on 1.14 because `mix.lock` pinned the older Finch 0.20.0. Raising the floor (rather than capping Finch, which would pin consumers to an old release) makes the supported Elixir range honest. The CI matrix drops Elixir 1.14 accordingly.

- **Circuit breaker closed-state checks no longer round-trip through the GenServer** — `check_and_allow_request/2` now serves the common case (a closed or not-yet-created circuit) from a direct ETS read instead of a serializing `GenServer.call`. Only `:open` (time-based transition) and `:half_open` (attempt increment) still call the GenServer, since they coordinate writes. This removes the per-request single-mailbox bottleneck on the hot path under high concurrency. Behavior is unchanged; the brief closed→open transition window remains eventually-consistent, as it already was with async (`cast`) failure recording.

- **Rate limiter reimplemented with GCRA for a lock-free hot path** — `consume/2` previously serialized every request through a single `GenServer.call` (which also made the ETS table's `write_concurrency` moot, since the GenServer was the only writer). The token bucket is now stored as a single GCRA timestamp per bucket and consumed via a lock-free compare-and-swap (`:ets.select_replace/2`) in the caller process, eliminating the per-request bottleneck. The GenServer is reduced to owning the table and its periodic cleanup. The per-request `Application.get_env` lookup was also removed from the hot path (the global `:rate_limit` default is now read lazily, only for keys a request's config omits). Rate-limiting semantics (burst capacity, refill rate, `:wait`/`:error` strategies, adaptive coordination) are unchanged.

- **Circuit breaker sliding window now uses O(1) counters instead of an O(n) list** — Each result-recording cast previously prepended `{result, timestamp}` to a list and recomputed failure/total counts by traversing it (`Enum.count`/`length`) on every record and threshold check. The window is now a bounded FIFO of results with `failure_count`/`success_count` maintained incrementally, so recording and `should_open?` are O(1) regardless of `window_size`. The stored per-entry timestamp was dead data (never read) and has been dropped. Threshold semantics (absolute count, percentage-over-window, half-open recovery) are unchanged.

- **BREAKING: rate limiter introspection/sync API changed.** As a consequence of the single-timestamp GCRA representation: `RateLimiter.get_bucket_state/1` now returns the raw GCRA `tat` (an integer, monotonic µs) or `nil`, instead of a `{tokens, last_refill_ms}` tuple; `RateLimiter.get_info/1` now returns `%{tat_us: integer}` instead of `%{current_tokens:, last_refill_ms:}`; and `RateLimiter.update_from_headers/2` is now `update_from_headers/3`, taking the rate `config` (needed to position the timestamp from the server's reported remaining count). For a human-meaningful "tokens remaining" value, use `check_rate_limit/2`.

- **Logged JSON request/response bodies are now re-serialized in compact form** — As a result of the structural sanitization fix above, JSON bodies in logs are emitted as canonical compact JSON (`{"k":"v"}`) rather than preserving the original formatting/whitespace. Redaction behavior is unchanged for non-JSON bodies.

- **BREAKING: Sanitization config moved from `:logging` to a dedicated `:sanitization` namespace** — `sanitize_headers` and `sanitize_body_fields` are now read from `config :httpower, :sanitization` instead of `config :httpower, :logging`. Applications that customized these lists must move them:

  ```elixir
  # Before
  config :httpower, :logging,
    sanitize_headers: ["x-secret"],
    sanitize_body_fields: ["pan"]

  # After
  config :httpower, :sanitization,
    sanitize_headers: ["x-secret"],
    sanitize_body_fields: ["pan"]
  ```

  Other `:logging` options (`level`, `log_headers`, `log_body`) are unchanged, as are the `:sanitize_headers`/`:sanitize_body_fields` options passed directly to `HTTPower.Logger.attach/1`.

- **Internal `HTTPower.Middleware.Dedup` functions are now `@doc false`** — `deduplicate/2`, `complete/3`, `cancel/1`, and `hash/3` operate on the internal SHA256 request hash and were never part of the public surface, but appeared in the generated docs (with stale `HTTPower.RequestDeduplicator` examples). They are now hidden. No behavior or signature change.

- **Rate limiter `:wait` strategy adds wakeup jitter** — When many requests block on the same bucket waiting for a token to refill, they previously slept for the exact same computed interval and all woke simultaneously to retry the lock-free consume, contending on the same refilled token (thundering herd). The sleep now adds a small upward jitter (up to ~20%, min 1ms) to spread wakeups. Jitter is always additive so a waiter never wakes before a token is available, and `max_wait_time` accounting is unchanged (it tracks the base wait, not the jittered sleep).

### Deprecated

- **`HTTPower.Logger.sanitize_headers/1` and `HTTPower.Logger.sanitize_body/1`** — Now delegate to `HTTPower.Sanitizer` and emit a deprecation warning when called. Use `HTTPower.Sanitizer.sanitize_headers/1` and `HTTPower.Sanitizer.sanitize_body/1` instead. The delegations will be removed in a future release.

### Fixed

- **`HTTPower.Error.message/1` now renders `{:feature_error, module, reason}` as a readable message** — `"Middleware <Module> failed: <reason>"` instead of falling through to `inspect/1` and dumping the raw tuple.

- **Tesla adapter no longer raises when no client is configured** — Using the Tesla adapter without an `adapter_config` (e.g. `adapter: HTTPower.Adapter.Tesla` instead of the `{module, tesla_client}` tuple) raised an `ArgumentError`, breaking HTTPower's "never raises" contract. It now returns `{:error, %HTTPower.Error{reason: :missing_tesla_client}}` like every other error path.

- **Adapter `catch` branches now unwrap transport errors like the `rescue` branch** — A thrown (rather than returned or raised) `%Mint.TransportError{}`/`%Req.TransportError{}` left the `catch` clause in the Finch, Req, and Tesla adapters returning the wrapped struct instead of the bare reason atom, so `HTTPower.Retry` could not classify it as a retryable transport error. The `catch` clause now applies `unwrap_transport_error/1`, matching the `rescue` clause.

- **All adapters now normalize transport errors so they are retryable** — Transport failures were passed through as raw exception structs (`%Mint.TransportError{}` from Finch, `%Req.TransportError{}` from Req, and either from Tesla's underlying client). Because these are *returned* as `{:error, struct}` rather than raised, the adapters' `rescue`-based unwrapping never fired, so `HTTPower.Retry` — which only classifies bare reason atoms — never retried them; they surfaced as an opaque `%HTTPower.Error{reason: %…TransportError{}}`. All three adapters now unwrap the bare reason atom (e.g. `:timeout`, `:econnrefused`) on both the returned and raised error paths. The Req adapter additionally recognizes `%Req.TransportError{}` (previously only `%Mint.TransportError{}` was unwrapped).

- **Req adapter no longer crashes on `proxy: :system`** — `proxy: :system` (the default) raised `CaseClauseError{term: {:ok, :system}}`: Req forwards `connect_options[:proxy]` to `Mint.HTTP.connect/4`, which only accepts a `{scheme, address, port, opts}` tuple. Neither Mint nor Req has system-proxy auto-detection, so `:system` is now treated as "no explicit proxy" (direct connection), matching the Finch adapter.

- **Finch and Tesla adapters send no body instead of an empty string for bodyless requests** — both coerced a `nil` body to `""`, which emits `Content-Length: 0` on bodyless requests such as GET (RFC 9110 §9.3.1 says a GET should not include a body, and some servers reject it). `nil` is now passed through to `Finch.build/4` and `Tesla.Env`, both of which treat it as "no body".

- **Req adapter response headers are now consistently list-valued across adapters** — `convert_response` used `Map.new(headers)`, which on Req < 0.5 (where headers are a `[{k, v}]` tuple list) produced string-valued headers `%{k => v}`. `HTTPower.Codec`'s content-type detection only reads list-valued headers, so JSON responses would silently fail to decode under older Req. Header normalization is now shared via `HTTPower.Adapter.normalize_response_headers/1`, which always yields `%{k => [v]}` from either a tuple list or a map, matching the Finch and Tesla adapters.

- **Req adapter no longer silently drops a configured proxy** — `maybe_add_proxy_options` only forwarded `proxy` values that were keyword lists, so a Mint-style `{scheme, address, port, opts}` tuple (the format Mint actually requires) fell through to a catch-all and was discarded. Any explicit proxy value is now forwarded to Req's `connect_options[:proxy]`.

- **Circuit breaker no longer closes early from half-open when configured for multiple probes** — `maybe_transition_from_half_open_to_closed` counted window-wide successes, which still included successes recorded *before* the trip. With `half_open_requests > 1`, a single successful probe could already satisfy the threshold and close the circuit prematurely (masked by the default of `1`). Half-open probe successes are now tracked in a dedicated counter that resets on entering half-open, so only successes recorded while half-open count toward closing.

- **Telemetry handler no longer detaches itself on a malformed event** — `HTTPower.Logger`'s handler read `measurements.duration` directly with no error handling, and `:telemetry` permanently detaches any handler that raises — so one malformed event silently killed all subsequent logging. Handler bodies are now wrapped so an exception is logged as a warning and the event dropped, keeping the handler attached.

- **`retry_count` in `[:httpower, :request, :stop]` telemetry now reflects actual retries** — the metadata read a `:retry_count` option that was never written, so it was always `0`. `HTTPower.Retry.execute_with_retry` now returns `{result, retry_count}` and `HTTPower.Client` threads the real count into the stop metadata.

- **`can_do_request?` guards the `HTTPower.Test` lookup** — when `test_mode: true`, the test-mode check called `HTTPower.Test.mock_enabled?/0` unguarded, which would raise if the test module were not loaded. It is now wrapped in `Code.ensure_loaded?(HTTPower.Test)`, mirroring `HTTPower.TestInterceptor`.

- **Adaptive rate-limiting state no longer leaks for circuits that never recover** — the rate limiter records a per-circuit `{:adaptive_state, key}` row while a circuit is degraded and only clears it once a later request observes the circuit healthy. If traffic to that key stopped while the circuit was still open/half-open, the row was never cleared, and periodic cleanup skipped it (it matched only timestamped bucket rows). Adaptive-state rows now carry a timestamp that is refreshed on each degraded observation, and cleanup reaps any row idle past the bucket TTL.

### Security

- **Reject CR/LF/NUL in request header names and values** — Header data derived from untrusted input could carry `\r\n` and inject additional headers or split the request (CRLF injection). `HTTPower.Client` now validates header names and values before dispatch and returns `{:error, %HTTPower.Error{reason: :invalid_header}}` if any contains a carriage return, line feed, or NUL byte. Enforced above the adapter layer, so the guarantee — and the clean error (never a raise) — is identical across the Finch, Req, and Tesla adapters, regardless of whether the underlying client also validates.

- **Added dependency vulnerability auditing to CI** — Added `mix_audit` (dev/test) and wired `mix deps.audit` (known CVEs) plus `mix hex.audit` (retired packages) into the CI `deps_check` job, so vulnerable or retired dependencies fail the build. The first run surfaced a real issue: the locked `plug` was on a version affected by a high-severity multipart-header DoS advisory (GHSA-468c-vq7p-gh64); `mix.lock` is bumped to a patched `plug` release.

- **Fixed PCI data leak in telemetry metadata** — Request/response headers and bodies were emitted into the `[:httpower, :request, :start | :stop]` telemetry metadata **unsanitized**. Redaction only happened inside `HTTPower.Logger`'s own handler, so every other telemetry consumer (Datadog/OpenTelemetry exporters, custom handlers) received credit card numbers, CVVs, `Authorization` headers, `Set-Cookie`, and full bodies in the clear. Sanitization now happens at the emission boundary in `HTTPower.Client`, so all telemetry consumers receive redacted metadata. The URL was already sanitized; headers and bodies now are too.
- **Fixed PCI data leaks in request/response body logging** — Sensitive values could survive sanitization and reach logs in three cases: (1) CVV/CVC codes in form-encoded bodies (e.g. `cvv=123`) were not redacted because the `=` separator was missing from the CVV pattern; (2) sensitive fields whose value was a nested JSON object (e.g. `{"token": {"access": "..."}}`) leaked entirely because field-name redaction only matched scalar values; (3) JSON string values containing escaped quotes were partially redacted, leaking the remainder and corrupting the logged JSON. Binary JSON bodies are now parsed and sanitized structurally via the recursive map path before re-encoding, which correctly handles nested objects, arrays, and escaped characters. Non-JSON bodies (e.g. form-encoded) continue to use regex-based sanitization, now with the corrected CVV pattern.

- **Broadened CVV and card-number sanitization** — The CVV keyword set now also covers Amex `cvn`/`cid` and `card_cvv`/`security-code` (the digit-count constraint keeps ambiguous keywords like `cid` from matching longer non-CVV values); configured sensitive fields are now redacted in form-encoded (`field=value`) bodies, not just JSON; and Luhn-valid card numbers stored as **integers** (e.g. a numeric JSON value or map value under an unrecognized field) are now redacted, where previously only binary values were scanned.

- **Redact secret-shaped path segments in telemetry URLs** — `sanitize_uri_for_telemetry/1` already stripped query/fragment but left the path intact, so a secret embedded in a URL path (e.g. `/v1/keys/sk_live_.../rotate`) leaked into request telemetry metadata. Path segments matching known provider key prefixes (Stripe `sk_`/`pk_`/`rk_`/`whsec_`, GitHub `ghp_`/`github_pat_`/…, Slack `xox*-`, AWS `AKIA…`) are now replaced with `[REDACTED]`. The match is deliberately conservative so ordinary resource ids and slugs (UUIDs, `cus_123`) stay intact for useful, low-cardinality metrics.

- **Rate-limit header parsing no longer raises on an out-of-range reset** — A malicious or buggy server sending an unrepresentable `*-ratelimit-reset` value (beyond year 9999) made `HTTPower.RateLimitHeaders.parse/2` raise via `DateTime.from_unix!/1`, breaking the never-raises contract. Out-of-range timestamps are now treated as "no usable rate-limit info" (`{:error, :not_found}`) instead.

## [0.22.0] - 2026-03-19

### Added

- **`params:` option for query string encoding** — Pass `params: [page: 1, per: 20]` to append query parameters to the request URL. Merges with existing query strings. Accepts keyword lists, maps, or lists of two-element tuples (flat key-value only). Can be combined with any body option (`json:`, `form:`, `body:`).

### Changed

- **Reframed project positioning as a production reliability layer** — Updated README, package description, moduledoc, and roadmap to position HTTPower as a reliability layer for existing HTTP clients (Finch, Req, Tesla) rather than as another HTTP client library. README now leads with a hero code example showing the client pattern with explicit reliability options.

## [0.21.0] - 2026-03-18

### Added

- **`json:` option for automatic JSON request body encoding** — Pass `json: %{key: value}` to encode the body as JSON, set `Content-Type: application/json`, and set `Accept: application/json` in one step. Replaces the previous pattern of manually calling `Jason.encode!` and setting headers.

- **`form:` option for automatic form-urlencoded request body encoding** — Pass `form: %{key: value}` to encode the body as URL-encoded form data and set `Content-Type: application/x-www-form-urlencoded` automatically.

- **`raw:` option to skip automatic response body decoding** — Pass `raw: true` to receive the raw binary response body, bypassing Content-Type-driven decoding.

- **`HTTPower.Codec` module for adapter-independent body encoding/decoding** — Centralizes request encoding (`json:`, `form:`) and Content-Type-driven response decoding (auto-decodes `application/json` and `+json` suffix responses). Lives above the adapter layer; called from `HTTPower.Client`.

- **New error reasons `:conflicting_body_options` and `:json_encode_error`** — `:conflicting_body_options` is returned when more than one of `json:`, `form:`, or `body:` is provided for the same request. `:json_encode_error` is returned when `json:` encoding fails.

### Changed

- **Response decoding is now consistent across all adapters** — Finch, Req, and Tesla all decode response bodies via `HTTPower.Codec`, which applies Content-Type-driven JSON decoding. Previously, adapters had divergent behavior (Finch decoded all bodies as JSON; Req used its own decoding; Tesla depended on user middleware).

- **BREAKING: POST requests no longer get a default `Content-Type: application/x-www-form-urlencoded`** — Use the new `form:` option or set the header explicitly. This default was added in v0.1.0 but conflicted with the new `json:` and `form:` encoding options and was inconsistent with other HTTP methods. Applications that relied on this default must add `form: params` or set the header explicitly.

- **BREAKING: Finch adapter returns raw binary bodies for non-JSON responses** — Previously, non-JSON responses passed through Finch's blind JSON decode attempt (which would fail silently). Now they are returned as binary unless the `Content-Type` header indicates JSON.

- **BREAKING: Tesla users with `Tesla.Middleware.JSON` must remove it** — Having both `Tesla.Middleware.JSON` and `HTTPower.Codec` in the stack results in double-decoding of JSON responses.

- **BREAKING: Telemetry `request.body` metadata contains encoded JSON string when `json:` is used** — When using the `json:` option, the telemetry metadata reflects the encoded JSON string body rather than the original data structure.

- **Normalize adapter callback URL type to `URI.t()`** — The adapter callback spec now correctly reflects `URI.t()` instead of `String.t()`. `call_adapter` normalizes strings to URI as a safety net. Finch and Req accept `URI.t()` natively; Tesla converts internally.

- **Cache rate limiter default config at compile time** — `get_strategy/1` and `get_max_wait_time/1` now use `@default_config` via `Application.compile_env` instead of calling `Application.get_env` per request.

- **Move `@cleanup_interval` to top of Dedup module** — Grouped with other module attributes for consistency.

- **Remove no-op pipeline order test** — The test in `coordination_test.exs` asserted nothing and noted the behavior was already covered by dedup bypass tests.

### Removed

- **Finch adapter no longer blindly decodes all response bodies as JSON** — The Finch adapter previously attempted JSON decoding on every response regardless of Content-Type. Decoding is now delegated to `HTTPower.Codec` and is driven by the `Content-Type` response header.

- **Req adapter's built-in response decoding is disabled** — `HTTPower.Codec` handles all response decoding uniformly. Disabling Req's decoding prevents double-decoding and ensures consistent behavior across adapters.

### Fixed

- **CircuitBreaker.call/3 returns consistent error format** — Returns `%HTTPower.Error{reason: :service_unavailable}` instead of bare `:service_unavailable` atom, matching `handle_request/2` behavior.

- **Remove rescue-for-control-flow in RateLimiter** — `clear_adaptive_state/1` no longer uses `rescue ArgumentError -> :ok`. `:ets.delete/2` on a nonexistent key is a no-op; the rescue was masking potential table-missing errors.

- **Cap backoff exponent at 30** — Prevents wasteful big-integer math in `calculate_backoff_delay/2` when `max_retries` is set to high values. `2^30` (~1 billion ms) exceeds any practical `max_delay`.

- **Use default GenServer timeout for dedup** — `Dedup.deduplicate/2` no longer uses `:infinity` timeout. The default 5s timeout surfaces a stuck GenServer instead of hanging indefinitely.

- **Guard Logger against non-map headers** — `sanitize_if_enabled(:headers, ...)` now handles non-map input gracefully instead of crashing on `map_size/1`.

## [0.20.0] - 2026-03-08

### Added

- **Finch adapter unit tests** — Added 27 unit tests for `HTTPower.Adapter.Finch` covering HTTP
  methods, headers, SSL, proxy, response conversion, error handling, body handling, and
  `conn_opts` merging. Finch was the only adapter without unit tests.

### Changed

- **Extract shared `prepare_headers/2` into `HTTPower.Adapter`** — Consolidated duplicate header
  preparation logic from Finch, Req, and Test modules into a single public function in the
  `HTTPower.Adapter` behaviour module.

- **Cache sanitization field lists in Logger at attach time** — Sanitization header and body field
  lists are now normalized once during `HTTPower.Logger.attach/0` and stored in the handler config,
  eliminating repeated `Application.get_env` + normalization on every telemetry event.

- **Logger sanitizes once per event** — Headers and body are now sanitized once and reused for both
  structured metadata and log message formatting, eliminating redundant sanitization work.

- **Document `HTTPower.new/1` raises on invalid profile** — Clarified that the "never raises"
  principle applies to HTTP operations, not configuration errors. `new/1` raises `ArgumentError`
  for unknown profiles, which is correct Elixir convention.

- **Bump ExDoc to `~> 0.40`** — Enables automatic `llms.txt` generation for LLM-friendly
  documentation on HexDocs.

- **Document compile-time config caching in Client** — Added moduledoc section explaining that
  default adapter and middleware settings are cached at compile time and require recompilation to
  change.

- **Runtime Plug check in `HTTPower.Test`** — Response helpers (`json/2`, `html/2`, `text/2`,
  `transport_error/2`) now raise a clear error message if Plug is not installed, instead of
  crashing with `UndefinedFunctionError`.

### Fixed

- **Dedup `pid_to_hash` overwrite when process tracks multiple hashes** — Changed from a single
  hash per PID to a `MapSet` of hashes. Previously, if a process made concurrent requests with
  different bodies, only the last hash was tracked, leaking earlier entries on process death.

- **`max_retries` off-by-one** — `max_retries: 3` now correctly performs 3 retries (4 total
  attempts). Previously it performed only 2 retries due to a `<` vs `<=` comparison error.

- **Req adapter leaks HTTPower-specific options to Req** — Expanded the option filter list from
  12 to 20 entries, adding `:adapter_config`, `:circuit_breaker`, `:circuit_breaker_key`,
  `:deduplicate`, `:profile`, `:rate_limit`, `:rate_limit_key`, and `:request_steps`.

- **DELETE method ignores request body** — `HTTPower.delete/2` now extracts and forwards the
  `:body` option, matching the behavior of POST, PUT, and PATCH.

- **Dedup wait telemetry reports actual wait duration** — `wait_time_ms` in `[:httpower, :dedup,
  :wait]` events now reflects the actual time spent waiting for the original request, instead of
  always reporting 0.

- **`circuit_breaker_key` works as top-level request option** — Previously only recognized when
  nested inside `circuit_breaker: [circuit_breaker_key: "..."]`. Now works as documented:
  `HTTPower.get(url, circuit_breaker_key: "payment_api")`. Also fixed in the rate limiter's
  adaptive rate adjustment.

- **Removed unreachable `{:error, {:http_status, ...}}` pattern** — Dead code in
  `Client.get_request_function/2` telemetry metadata that could never match, since the retry
  module converts HTTP status errors to `{:ok, response}`.

- **`parse_retry_after/1` normalizes header keys** — Now performs case-insensitive header lookup,
  matching the behavior of `parse/2`. Previously, mixed-case keys like `"Retry-After"` would not
  be found when calling `parse_retry_after/1` directly.

## [0.19.0] - 2026-03-07

### Added

- **Dedup abort telemetry event** — New `[:httpower, :dedup, :abort]` telemetry event emitted
  when the original requester dies or cancels, notifying waiting processes of the abort.

### Changed

- **Short-circuit `can_do_request?` when test_mode is false** — Skips mock/plug checks entirely
  when test mode is disabled, avoiding unnecessary work in production.

- **Removed redundant `Logger.debug` from rate limiter wait path** — The debug log in the
  wait-and-retry loop was redundant with the telemetry event already emitted.

- **Removed redundant URI.parse in Finch adapter** — `maybe_add_ssl_options/3` now uses the
  `%URI{}` struct directly instead of re-parsing it from a string.

- **Consolidated retry attempt count check** — Retry attempt validation is now handled in a
  single location rather than being duplicated.

- **Set explicit `failure_threshold` in profiles** — Configuration profiles now set
  `failure_threshold` explicitly to match their percentage-based intent.

- **Credit card sanitization now uses Luhn validation** — The regex pattern finds candidates
  (13-19 digit sequences), then Luhn checksum filters out non-card numbers like order IDs
  and timestamps. Reduces false positives by ~90% while maintaining PCI safety. JSON
  field-name sanitization (e.g., `"credit_card": "value"`) remains unconditional.

### Fixed

- **Dedup waiters hanging when original requester dies or request is cancelled** — Waiters now
  receive an abort message and re-issue the request instead of hanging indefinitely.

- **Logger credit card regex now handles all PAN formats** — The sanitization regex now matches
  any 13-19 digit sequence regardless of grouping (AmEx non-standard grouping, old 13-digit
  Visa, extended 19-digit PANs), not just 4+4+4+N formatted cards.

- **Logger JSON field sanitization now redacts non-string values** — Sensitive JSON fields with
  numeric (`"pin": 1234`), boolean (`"secret": true`), or null values are now properly
  redacted, not just double-quoted string values.

### Removed

- **Removed redundant tests** — Eliminated ~30 overlapping tests: exact duplicate transport
  error tests, redundant "no logs when not attached" logger tests, adapter integration tests
  that only exercised HTTPower.Test interception rather than actual adapter code, and SSL/proxy
  smoke tests already covered by adapter unit tests.

## [0.18.0] - 2026-03-06

### Added

- **Cross-process mock support in HTTPower.Test** — Mocks are now visible to processes
  spawned from the test (`Task.async`, `Task.async_stream`) via `$callers` chain walking.
  For pre-existing processes (GenServers, Agents), use the new `HTTPower.Test.allow/2`.
  Mock storage moved from process dictionary to ETS for cross-process accessibility.

## [0.17.0] - 2026-03-06

### Added

- **Direct test coverage for `HTTPower.Error` module** - Added unit tests for error struct
  creation and the `error_message/1` helper function.

- **Configurable dedup wait timeout** - The dedup `wait_timeout` option is now user-configurable
  via the `deduplicate` keyword list (e.g., `deduplicate: [wait_timeout: 30_000]`).

### Changed

- **Elixir 1.20.0-rc.2** - Updated `.tool-versions` to Elixir 1.20.0-rc.2.

- **Adaptive rate limiting telemetry only emits on state transitions** - The
  `[:httpower, :rate_limit, :adaptive_reduction]` telemetry event now only fires when the
  adaptive multiplier actually changes, reducing noise under sustained circuit breaker states.

- **Tesla header conversion aligned with Finch** - Tesla adapter now uses the same
  prepend+reverse approach as the Finch adapter for header conversion, ensuring consistent
  header ordering.

- **mix.exs description includes Finch** - Updated the package description to mention Finch
  as the default adapter.

- **Retry documentation improvements** - Clarified the distinction between retryable HTTP
  status codes and transport errors. Documented that `{:ok, response}` may include 5xx
  responses after retries are exhausted.

### Fixed

- **Elixir 1.14 test failures due to missing telemetry dependency** - Added `:telemetry` as a
  direct dependency. It was used in 6 source files but only available transitively through
  optional deps (Finch, Req, Plug), causing 69 test failures on Elixir 1.14 where the telemetry
  application wasn't auto-started.

- **Req.Test.Ownership not started on Elixir 1.14** - SSL+proxy tests that use `Req.Test.stub/2`
  directly now explicitly start the Req application, fixing 2 test failures on Elixir 1.14.

- **Circuit breaker now records 5xx and 429 responses as failures** - Previously,
  `handle_post_request` treated any `{:ok, response}` as a success, including 5xx and 429
  responses returned after retry exhaustion. This meant sustained server errors would never
  trip the circuit breaker. Now responses with `status >= 500` or `status == 429` are correctly
  recorded as failures.

- **Rate limiter `:wait` strategy re-checks tokens after sleeping** - Previously, the `:wait`
  strategy would sleep and then consume without re-checking availability, which could lead to
  negative token counts under concurrency. Now it re-checks after waking up.

- **`handle_info` catch-all in RateLimiter and Dedup GenServers** - Added catch-all
  `handle_info/2` clauses to prevent crashes from unexpected messages.

- **`Error.reason` type spec includes tuple variants** - Fixed the typespec to reflect that
  `reason` can be a tuple (e.g., `{:circuit_breaker, :open}`), not just an atom.

- **Removed unreachable `error_message/1` catch-all clause** - Cleaned up dead code in the
  Error module.

- **Client uses `request.headers` instead of re-extracting from opts** - Fixed the client to
  use the already-built request headers rather than re-reading raw options.

## [0.16.0] - 2026-02-18

### Fixed

- **Race condition in RateLimiter.consume/2** - `consume/2` previously used two separate
  GenServer calls (`check_rate_limit` then `consume_token`). Under concurrency, multiple
  processes could pass the check before any consumed, allowing over-consumption and negative
  token counts. Replaced with a single atomic `{:check_and_consume, ...}` GenServer call
  that checks availability and subtracts in one step. Extracted shared `refill_bucket/3`
  helper to reduce duplication between `check_rate_limit` and `check_and_consume`.

- **Documented dedup waiter ref-sharing edge case** - Added comments explaining the shared
  reference mechanism in request deduplication and its safe timeout fallback when ETS entries
  are cleaned up between deduplicate and receive.

- **Req adapter connect_options overwrite** - `maybe_add_ssl_options` and
  `maybe_add_proxy_options` now merge into existing `:connect_options` instead of overwriting.
  Previously, configuring both SSL and proxy on an HTTPS request would lose the SSL settings.

- **Inconsistent response header format across adapters** - Tesla adapter now normalizes
  response headers to `%{"key" => ["value"]}` (list values), matching Finch and Req. The
  `Response.t()` type spec is updated to `%{optional(String.t()) => [String.t()]}`.

- **Hardcoded `connection: close` header removed** - Finch and Req adapters no longer inject
  `connection: close` on every request. This was defeating HTTP connection pooling. Connection
  management is now left to the underlying client.

- **URL double-slash with trailing base_url slash** - `build_url("https://api.com/", "/users")`
  now produces `https://api.com/users` instead of `https://api.com//users`.

- **Telemetry `circuit_key: "unknown"` fallback removed** - `emit_state_change_event` now
  requires a real circuit key. All call sites updated to pass the actual key.

### Added

- **PATCH, HEAD, and OPTIONS HTTP methods** - Added `HTTPower.patch/2`, `HTTPower.head/2`,
  and `HTTPower.options/2` to the public API, with full client instance support.

- **`HTTPower.Config` module** - Standardized config resolution (request → application env →
  default) used by CircuitBreaker and RateLimiter, replacing inconsistent ad-hoc patterns.

### Changed

- **Plug is now an optional dependency** - Changed from `only: :test` to `optional: true` so
  library consumers can use `HTTPower.Test` without independently adding Plug to their deps.

- **Dedup cleanup interval increased from 1s to 5s** - Reduces GenServer message processing
  overhead. Completed entries with a 500ms TTL sit harmlessly in memory for a few extra seconds
  before being cleaned up.

- **Updated moduledocs** - `HTTPower` and `HTTPower.Adapter` moduledocs now correctly describe
  the adapter-based architecture with Finch as the default.

- **Mint.TransportError decoupled from core** - Adapters now unwrap `Mint.TransportError`
  structs to plain atoms at the adapter boundary. `HTTPower.Error` and `HTTPower.Retry` no
  longer depend on Mint directly.

- **CircuitBreaker catch-all `handle_info/2`** - Silently ignores unexpected messages instead
  of logging GenServer warnings.

- **Generic deep merge for profile options** - `deep_merge_options` now uses `Keyword.keyword?/1`
  instead of a hardcoded key list, automatically deep-merging any nested keyword list config.

## [0.15.2] - 2026-02-06

### Added

- **Retry-After HTTP date format support (RFC 7231)** - `parse_retry_after/1` now parses
  IMF-fixdate values (e.g., `Wed, 21 Oct 2015 07:28:00 GMT`) in addition to integer seconds.
  Converts the date to seconds-until-that-time for use in retry backoff. Dates in the past
  are treated as missing. Previously only integer seconds were supported.

### Changed

- **CI matrix updated for Elixir 1.19 / OTP 28** - Added Elixir 1.19 and OTP 28 to test
  matrix, dropped OTP 25. Format, deps, and coverage jobs now run on latest versions.
- **Credo strict compliance** - Resolved all 14 credo strict issues: implicit try in adapters,
  reduced function nesting/complexity, sorted aliases, removed redundant with clause, replaced
  apply/3 with direct call, and other cleanups.
- **Telemetry warning fix** - Replaced anonymous function handlers with module-qualified
  captures to eliminate `:telemetry` performance warnings.
- **Code simplification** - Consolidated duplicate `error_message/1` into `Error.message/1`,
  removed dead code, simplified control flow. Net -159 lines.

## [0.15.1] - 2025-10-19

### Changed

- **Relaxed optional dependency version requirements to prevent conflicts**
  - Changed Finch from `~> 0.20` to `>= 0.19.0` (optional)
  - Changed Req from `~> 0.4.0` to `>= 0.4.0` (optional)
  - Changed Tesla from `~> 1.11` to `>= 1.10.0` (optional)
  - Prevents version conflicts in consuming applications that need different HTTP client versions
  - For example, apps using Req 0.5+ alongside HTTPower will no longer encounter dependency conflicts
  - Consuming apps should specify their preferred HTTP client versions in their own mix.exs

### Technical Details

- Optional dependencies now use `>=` instead of `~>` to maximize flexibility
- Minimum version requirements still ensure compatibility with HTTPower's adapter code
- All 368 tests passing with current dependency versions
- This change particularly helps when multiple packages in an app have conflicting requirements for the same HTTP client

## [0.15.0] - 2025-10-19

### Fixed

- **Critical: HTTPower.Test mocking now works when HTTPower is installed from Hex**
  - Fixed `HTTPower.TestInterceptor` to use runtime checks instead of compile-time `Mix.env()` check
  - Test interception now works regardless of how HTTPower was compiled (hex package or local path)
  - Changed from `if Mix.env() == :test` to runtime `Code.ensure_loaded?(HTTPower.Test) and HTTPower.Test.mock_enabled?()`
  - Enables `HTTPower.Test.stub/1` to work in consuming applications that install HTTPower from Hex
  - Previously, test mocking only worked when HTTPower was compiled locally in test environment

### Technical Details

- `TestInterceptor.test_enabled?/0` now checks if `HTTPower.Test` module is loaded AND mock is enabled at runtime
- No changes to test mode blocking logic in `HTTPower.Client` - that continues to work correctly
- Separation of concerns: test_mode config blocks REAL requests, TestInterceptor enables MOCKED requests
- All 368 tests passing with zero compilation warnings

## [0.14.0] - 2025-10-16

### Fixed

- **Critical: Conditional compilation for optional adapter dependencies**
  - Wrapped all adapter modules (Finch, Req, Tesla) with `if Code.ensure_loaded?/1`
  - Adapters now only compile when their dependencies are available
  - Fixes compilation errors when using HTTPower with only one adapter installed
  - Enables true optional dependencies - install only what you need
  - Standard Elixir pattern used by Phoenix, Ecto, and other ecosystem libraries

### Technical Details

- Adapter modules check dependency availability at compile time via `Code.ensure_loaded?/1`
- When dependency is missing, adapter module is not defined (compilation skipped entirely)
- Runtime adapter detection (`detect_adapter/0`) already uses `Code.ensure_loaded?/1` - no changes needed
- All 380 tests passing (368 tests + 12 doctests)
- Verified compilation with various dependency combinations (Finch only, Req only, all three)

## [0.13.0] - 2025-10-13

### Added

- **Intelligent middleware coordination** - Middleware now coordinate intelligently for dramatically improved system capacity
  - **Dedup bypasses rate limiting** - Cache hits skip rate limiting entirely (5x effective capacity improvement)
    - Middleware reordered: Dedup → RateLimiter → CircuitBreaker
    - Pipeline short-circuits on cache hits with `{:halt, response}`
    - Telemetry coordination metadata: `bypassed_rate_limit` measurement + `:rate_limit_bypass` in metadata
    - Zero coupling - pure pipeline architecture
  - **Adaptive rate limiting** - Dynamically adjusts rates based on downstream health (prevents thundering herd)
    - When circuit breaker opens (service down) → 10% of normal rate
    - When circuit breaker half-open (recovering) → 50% of normal rate
    - When circuit breaker closed (healthy) → 100% of normal rate
    - Enabled by default with `adaptive: true` in rate limit config
    - Read-only state queries preserve loose coupling
    - Telemetry events: `[:httpower, :rate_limit, :adaptive_reduction]`
  - **Configuration profiles** - Pre-configured settings for common use cases
    - `:payment_processing` - Conservative settings for payment gateways (Stripe, PayPal)
      - 100 req/min, 30% failure threshold, 5s dedup window
      - Prevents double charges and duplicate orders automatically
    - `:high_volume_api` - High throughput for public APIs
      - 1000 req/min, 50% failure threshold, 1s dedup window
      - Maximum throughput with dedup providing 5x capacity boost
    - `:microservices_mesh` - Balanced settings for service-to-service calls
      - 500 req/min, 40% failure threshold, 2s dedup window
      - Prevents cascade failures and retry storms
    - `HTTPower.Profiles.list/0` - List all available profiles
    - `HTTPower.Profiles.get/1` - Get profile configuration
    - Profiles support option overrides with deep merge (nested options preserved)

### Changed

- **Middleware pipeline handling improvements**
  - Fixed pipeline short-circuit bug where `{:halt, response}` was incorrectly processed
  - Pipeline now correctly distinguishes between `{:ok, %Request{}}` (continue) and `{:halt, %Response{}}` (short-circuit)
  - Removed obsolete `handle_pipeline_result/1` functions (replaced with direct pattern matching)
  - Cleaner error handling for middleware failures

- **Profile configuration with deep merge**
  - User options now deep merge with profile defaults for nested keys (`:rate_limit`, `:circuit_breaker`, `:deduplicate`)
  - Example: `profile: :high_volume_api, rate_limit: [requests: 2000]` preserves `per: :minute` from profile
  - Prevents accidental override of entire nested config
  - Explicit options always win over profile defaults

### Technical Details

- **Coordination patterns**
  - Pattern A: Dedup bypasses rate limiting through pipeline ordering (0 lines of coupling code)
  - Pattern B: Adaptive rate limiting via read-only state queries (loose coupling preserved)
  - Deep merge algorithm handles nested keyword lists correctly
- **Pipeline short-circuit fix**
  - Changed from `with` statement to `case` statement for clearer flow control
  - Direct pattern matching on `{:ok, %Request{}}` vs `{:halt, %Response{}}`
  - Post-request handlers only called when HTTP actually executed
- **Test improvements**
  - 14 new coordination tests (all passing)
  - Tests use synchronous requests to avoid process dictionary issues
  - Comprehensive coverage of all coordination patterns
- **Performance impact**
  - Dedup cache hits: 5x effective capacity (skip rate limiter entirely)
  - Adaptive rate limiting: Prevents thundering herd during recovery
  - Zero overhead for disabled features (compile-time pipeline)
- All 371 tests passing (14 new coordination tests)
- Zero compilation warnings
- Comprehensive telemetry for observability

## [0.12.0] - 2025-10-13

### Added

- **Finch adapter** - High-performance HTTP client built on Mint and NimblePool
  - `HTTPower.Adapter.Finch` - New adapter using Finch HTTP client
  - Performance-focused with explicit connection pooling
  - Built on Mint (same low-level library that powers Req)
  - SSL/TLS support with configurable verification
  - Proxy support (system or custom)
  - Manual JSON decoding with Jason for flexibility
  - Connection pool configuration via Application config
  - Comprehensive test suite (357 tests passing)

### Changed

- **Finch is now the default adapter** (breaking change for adapter detection order)
  - Adapter detection priority: Finch → Req → Tesla
  - All adapters remain optional - users can choose what to install
  - No breaking changes to existing code (auto-detection still works)
  - Updated error message to recommend Finch first
  - Updated README and documentation to reflect Finch as default
- **Renamed Feature to Middleware for clearer semantics**
  - `HTTPower.Feature` → `HTTPower.Middleware` (module namespace change)
  - `lib/httpower/feature/` → `lib/httpower/middleware/` (directory reorganization)
  - All middleware modules moved: `RateLimiter`, `CircuitBreaker`, `Dedup`
  - Updated all references in code, tests, and documentation
  - Industry-standard terminology (matches Phoenix/Plug, Express, Rails)
  - Zero breaking changes to public API - internal refactoring only
- **Logger tests updated to use Finch adapter**
  - Converted from `Req.Test` to `HTTPower.Test` (adapter-agnostic)
  - All 51 logger tests now test with Finch adapter
  - Tests demonstrate adapter-agnostic testing approach

### Technical Details

- Finch adapter handles both URI structs and strings
- Header format conversion to match Req's format (map with list values)
- Automatic JSON body parsing when response is valid JSON
- Conditional supervision - Finch only started if loaded via `Code.ensure_loaded?/1`
- Default pool configuration: 10 connections per pool, one pool per scheduler
- Configurable pools via `config :httpower, :finch_pools`
- Test coverage excludes Finch adapter (tested via integration tests)
- All production features work consistently across Finch, Req, and Tesla adapters
- Middleware pattern provides clearer mental model for request/response pipeline
- All 357 tests passing with zero compilation warnings

## [0.11.0] - 2025-10-13

### Changed

- **Refactored client to extensible pipeline architecture**
  - Introduced `HTTPower.Feature` behaviour for composable request pipeline features
  - New `HTTPower.Request` struct for context passing between pipeline stages
  - Generic recursive step executor works with ANY feature implementation
  - Compile-time feature registry with zero overhead for disabled features
  - Runtime config merging (runtime takes precedence over compile-time)
  - Features can inspect, modify, short-circuit, or fail requests
  - Clean separation: features communicate via `request.private` map

- **URL validation and URI struct improvements**
  - Early fail-fast URL validation with clear error messages
  - Parse URL once at request start, use URI struct throughout pipeline
  - Direct field access: `request.url.host` instead of helper functions
  - SSL check now uses pattern matching: `%URI{scheme: "https"}`
  - Eliminates repeated URL parsing for better performance

- **Feature implementations refactored to use pipeline architecture**
  - All features (RateLimiter, CircuitBreaker, Dedup) now implement `HTTPower.Feature` behaviour
  - Simplified key extraction: `request.url.host` instead of URL parsing
  - Features store state in `request.private` for post-request processing
  - Circuit breaker and dedup can short-circuit pipeline with `{:halt, response}`
  - More consistent error handling across all features

- **Request execution flow improvements**
  - Request struct now built early in request lifecycle and passed throughout pipeline
  - Cleaner parameter passing: 2 parameters instead of 6 in execution functions

- **Code cleanup and documentation**
  - Removed ~57 redundant comments that described what code does rather than why
  - Kept important comments explaining architectural decisions
  - Client.ex, RateLimiter.ex, CircuitBreaker.ex, Dedup.ex all cleaned up
  - Improved code readability while maintaining comprehensive inline documentation

### Technical Details

- **Pipeline execution**: Features run in order (RateLimiter → CircuitBreaker → Dedup)
- **Zero overhead**: Disabled features not included in compiled pipeline
- **Post-request cleanup**: Circuit breaker recording and dedup completion handled automatically
- **Extensibility**: Adding new features requires only implementing `HTTPower.Feature` behaviour
- **Type safety**: URI structs ensure valid URLs throughout the pipeline
- **Testability**: Generic pipeline executor simplifies testing of new features
- **Request flow**: Request struct created early (line 103), passed through entire pipeline
- All 348 tests passing
- Zero compile warnings
- Net code addition: 434 lines (+721 -287) with significant architectural improvements

## [0.10.0] - 2025-10-07

### Added

- **Structured logging with metadata for log aggregation**
  - All log entries now include structured metadata via `Logger.metadata()`
  - Request metadata: `httpower_correlation_id`, `httpower_event`, `httpower_method`, `httpower_url`, headers, body
  - Response metadata: `httpower_correlation_id`, `httpower_event`, `httpower_status`, `httpower_duration_ms`, headers, body
  - Exception metadata: `httpower_correlation_id`, `httpower_event`, `httpower_duration_ms`, `httpower_exception_kind`, `httpower_exception_reason`
  - Enables powerful querying in log aggregation systems (Datadog, Splunk, ELK, Loki)
  - Query examples: `httpower_duration_ms:>1000`, `httpower_status:>=500`, `httpower_correlation_id:"req_abc123"`
  - All metadata respects `log_headers` and `log_body` configuration
  - Large bodies automatically truncated to 500 characters in metadata
  - All sensitive data sanitized before adding to metadata
  - Added 9 comprehensive tests for metadata functionality

### Changed

- **Performance: ETS write concurrency optimization**
  - Added `{:write_concurrency, true}` to all ETS tables (CircuitBreaker, RateLimiter, Dedup)
  - Expected 2-3x throughput improvement under high concurrency (50+ concurrent requests)
  - Enables parallel writes across multiple processes without serialization
  - Production-grade performance for high-traffic scenarios

- **Performance: CircuitBreaker async recording**
  - Switched from synchronous `GenServer.call` to async `GenServer.cast` for result recording
  - Expected 5-10x improvement in high-throughput scenarios
  - Non-blocking operation: requests don't wait for state updates
  - Eventually consistent state (5-10ms delay acceptable for circuit breaker logic)
  - Updated tests to handle async state changes with polling helper

- **Performance: Configuration caching optimization**
  - Implemented compile-time config caching using `Application.compile_env`
  - Eliminates repeated `Application.get_env` calls on every request
  - Module attributes cache default config values at compile time
  - Config resolution order: request-level → compile-time cached → runtime → hardcoded default
  - Runtime fallback ensures tests can dynamically override config
  - Particularly beneficial for high-throughput scenarios

### Technical Details

- ETS concurrency: `{:write_concurrency, true}` uses distributed locks for better parallelism
- CircuitBreaker: Test updates include `await_state/3` helper for polling async state changes
- Config caching: `@default_adapter`, `@default_config`, `@default_failure_threshold`, etc. cached at compile time
- Structured logging: Machine-readable metadata fields for production observability
- All 348 tests passing (9 new tests for structured logging metadata)
- Comprehensive documentation updates across README, guides, and CLAUDE.md

## [0.9.0] - 2025-10-06

### Added

- **Comprehensive telemetry integration using Erlang's `:telemetry` library**
  - HTTP request lifecycle events: `[:httpower, :request, :start]`, `[:httpower, :request, :stop]`, `[:httpower, :request, :exception]`
  - Retry attempt events: `[:httpower, :retry, :attempt]` with attempt_number, delay_ms, and reason
  - Rate limiter events: `[:httpower, :rate_limit, :ok]`, `[:httpower, :rate_limit, :wait]`, `[:httpower, :rate_limit, :exceeded]`
  - Circuit breaker events: `[:httpower, :circuit_breaker, :state_change]`, `[:httpower, :circuit_breaker, :open]`
  - Deduplication events: `[:httpower, :dedup, :execute]`, `[:httpower, :dedup, :wait]`, `[:httpower, :dedup, :cache_hit]`
  - All events include rich measurements (duration, timestamps) and metadata (method, url, status, etc.)
  - URLs automatically sanitized (query params/fragments stripped) for low cardinality in metrics
  - Default ports (80/443) excluded from URL telemetry for cleaner metrics
  - Zero dependencies (`:telemetry` ships with Elixir)
  - Full observability guide with Prometheus, OpenTelemetry, and LiveDashboard examples

**Integration Examples:**

```elixir
# Prometheus metrics
distribution("httpower.request.duration",
  event_name: [:httpower, :request, :stop],
  measurement: :duration,
  unit: {:native, :millisecond},
  tags: [:method, :status]
)

# OpenTelemetry
OpentelemetryTelemetry.register_application_tracer(:httpower)

# Custom logging
:telemetry.attach("httpower-logger", [:httpower, :request, :stop], &log_request/4, nil)
```

**Documentation:**

- Added comprehensive observability guide at `guides/observability.md`
- Updated README with Observability & Telemetry section
- Added 11 new telemetry integration tests (339 total tests, all passing)

## [0.8.1] - 2025-10-01

### Fixed

- **Documentation updates for v0.8.0 breaking changes**
  - Updated all error atom references in README examples
  - Updated configuration reference with new error atoms
  - Updated migration guides (Tesla and Req)
  - Updated production deployment guide examples
  - Restructured Configuration Availability Matrix for better HTML rendering
  - All documentation now correctly references `:too_many_requests` and `:service_unavailable`

## [0.8.0] - 2025-10-01

### Changed

- **BREAKING: Plug-compatible error atoms for Phoenix integration**
  - Changed `:rate_limit_exceeded` → `:too_many_requests` (HTTP 429)
  - Changed `:circuit_breaker_open` → `:service_unavailable` (HTTP 503)
  - Enables seamless Phoenix/Plug integration without manual error mapping
  - HTTPower-specific atoms preserved: `:rate_limit_wait_timeout`, `:dedup_timeout`, transport errors
  - All error handling code must be updated to use new atoms

**Migration Guide:**

```elixir
# Update error pattern matching:
{:error, %{reason: :rate_limit_exceeded}}    # OLD
{:error, %{reason: :too_many_requests}}      # NEW

{:error, %{reason: :circuit_breaker_open}}   # OLD
{:error, %{reason: :service_unavailable}}    # NEW
```

## [0.7.1] - 2025-10-01

### Fixed

- **Critical: CircuitBreaker race condition in half-open state**
  - Fixed race condition where multiple concurrent processes could exceed `half_open_max_requests` limit
  - Increment counter BEFORE allowing request through (prevents concurrent bypass)
  - Added comprehensive concurrent request test (10 concurrent requests, verifies only 3 allowed)
- **Critical: ETS table orphaning on GenServer crash**
  - Fixed GenServer crashes orphaning ETS tables and causing supervisor restart loops
  - Added `{:heir, :none}` to all ETS table creations (RateLimiter, CircuitBreaker, Dedup)
  - Tables now automatically deleted when owning process terminates
  - Added crash recovery tests for all three GenServers
- **Critical: Dedup waiter timeout memory leak**
  - Fixed memory leak where dead/timeout waiter processes remained in memory indefinitely
  - Added process monitoring to detect when waiters die or timeout
  - Automatic cleanup removes dead waiters from in-flight request lists
  - Added tests for waiter death and timeout scenarios

### Changed

- **Performance: RateLimiter config caching**
  - Cache default configuration at GenServer startup (eliminates repeated `Application.get_env` calls)
  - ~15-20% reduction in rate limiter overhead per request
  - Config changes now require GenServer restart to take effect (production-realistic behavior)
  - Helper functions split into public (backward compatible) and optimized (GenServer) versions

### Technical Details

- All fixes based on comprehensive architectural review (see `doc/architecture-improvements.md`)
- CircuitBreaker: Inverted condition and atomic increment prevents race
- ETS tables: `{:heir, :none}` ensures clean supervisor restarts
- Dedup: Process monitors with `:DOWN` message handling for cleanup
- RateLimiter: Cached config in GenServer state, passed to callbacks
- All 328 tests passing with comprehensive coverage of new scenarios

## [0.7.0] - 2025-10-01

### Added

- **Request deduplication** - Prevent duplicate operations from double-clicks and race conditions
  - `HTTPower.Dedup` GenServer for tracking in-flight requests
  - Hash-based fingerprinting using method + URL + body
  - Response sharing: duplicate requests wait for first request to complete
  - Automatic cleanup of completed requests after 500ms TTL
  - Global configuration: `config :httpower, :deduplication, enabled: true`
  - Per-request control: `deduplicate: true` or `deduplicate: [key: custom_key]`
  - Custom deduplication keys for fine-grained control
  - Client-side protection that complements server-side idempotency keys
  - Integrated into request pipeline (after rate limit check, before circuit breaker)
  - 18 comprehensive tests covering hash generation, in-flight tracking, response sharing, cleanup, and high concurrency
- **Rate limit headers parsing** - Automatic detection and parsing of server rate limits from HTTP response headers
  - `HTTPower.RateLimitHeaders.parse/2` - Parses rate limit headers from responses
  - Supports multiple common formats:
    - GitHub/Twitter style: `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset`
    - RFC 6585/IETF style: `RateLimit-Limit`, `RateLimit-Remaining`, `RateLimit-Reset`
    - Stripe style: `X-Stripe-RateLimit-*` headers
    - `Retry-After` header (integer seconds format)
  - Auto-detection with `:auto` format (default), or explicit format specification
  - Case-insensitive header matching
  - Handles header values as strings, lists, or integers (adapter-agnostic)
- **Rate limiter integration with server headers** - Synchronize local rate limiter with server state
  - `HTTPower.RateLimiter.update_from_headers/2` - Updates bucket state from parsed headers
  - `HTTPower.RateLimiter.get_info/1` - Returns current bucket information
  - Server-provided limits synchronize with token bucket algorithm
  - Buckets continue to refill after synchronization

### Technical Details

- Header parser uses format auto-detection, trying GitHub → RFC → Stripe formats in order
- Parser handles all adapter header formats (Req's list of tuples, Tesla's various formats)
- Integration updates token bucket state to match server's remaining count
- Comprehensive test coverage: 38 tests for parser, 7 tests for integration, 5 tests for Retry-After
- HTTP date format in `Retry-After` supported since v0.15.2 (previously only integer seconds)
- **Automatic backoff** - Retry logic respects `Retry-After` header on 429/503 responses
  - When server provides `Retry-After` header (integer seconds), HTTPower uses that exact wait time
  - Falls back to exponential backoff when header is missing
  - Only applies to 429 (Too Many Requests) and 503 (Service Unavailable) status codes
  - Other retryable status codes (408, 500, 502, 504) continue using exponential backoff

### Changed

- **Code organization and readability improvements** in `HTTPower.Client`
  - Reorganized file into logical sections: Public API, Main Pipeline, Retry Logic, Adapters, Test Mode, Rate Limit Config, Circuit Breaker Config, Logging, Error Handling
  - Refactored request flow to use clean `with` pipelines instead of nested case statements
  - All helper functions now return explicit `{:ok, value}` or `{:error, reason}` tuples for consistency
  - Moved test mode check to beginning of request pipeline for fail-fast behavior
  - Removed redundant code comments (kept only user-facing messages and section headers)
  - Simplified parameter passing by extracting options at point of use
  - Eliminated unnecessary wrapper functions for cleaner call stack
  - Net reduction of 27 lines while improving code clarity

### Technical Details

- No performance changes - refactoring only improves maintainability
- Rate limiter and circuit breaker already have early-exit optimizations when disabled
- All 304 tests passing, 0 compile warnings
- Renamed `do_http_request/3` → `execute_http_request/3` for clarity
- Removed `do_request/3` wrapper that just delegated to `execute_http_request/3`

## [0.6.0] - 2025-09-30

### Added

- **Global adapter configuration** - Configure HTTP adapter application-wide
  - `config :httpower, adapter: HTTPower.Adapter.Req` for global adapter selection
  - `config :httpower, adapter: {HTTPower.Adapter.Tesla, tesla_client}` for pre-configured clients
  - Configuration priority: per-request > per-client > global
  - Allows adapter switching without code changes
- **Comprehensive documentation structure** in `guides/` directory
  - `guides/migrating-from-tesla.md` - Complete 7-step Tesla migration guide
    - Emphasizes adapter-agnostic final code
    - Shows HTTPower.Test for testing (not Tesla.Mock)
    - Global and per-client configuration examples
  - `guides/migrating-from-req.md` - Req migration guide with error handling differences
  - `guides/configuration-reference.md` - Complete option reference with availability matrix
  - `guides/production-deployment.md` - Production deployment guide with supervision tree, monitoring, security
  - Moved examples to `guides/examples/` directory
- **Configuration availability matrix** showing which options work at global/per-client/per-request levels
- Updated ExDoc integration with organized guide groups (Migration Guides, Guides, Examples)

### Changed

- **README improvements**
  - Simplified "Adapter Support" section (removed confusing adapter-specific examples)
  - Added "Perfect For" section at top showing target use cases
  - Updated "Basic Usage" to show both direct and client-based patterns
  - Split "Correlation IDs" into standalone section (not just PCI logging)
  - Removed redundant "Production Considerations" and "Why HTTPower?" sections
  - Updated all references from Req.Test to HTTPower.Test
- **Documentation corrections across all guides**
  - Fixed sanitization config structure: `sanitize_headers` and `sanitize_body_fields` (not nested under `sanitize:`)
  - Clarified that custom sanitization fields are additive (supplement defaults, not replace)
  - Updated all examples to use correct configuration structure

### Technical Details

- Global adapter configuration integrates with existing per-client/per-request options
- Adapter detection order: per-request option → global config → auto-detection (Req preferred)
- Documentation now correctly reflects implementation details
- All configuration examples consistent across README, guides, and reference docs

## [0.5.0] - 2025-09-30

### Added

- **Circuit breaker pattern implementation** for protecting against cascading failures
- `HTTPower.CircuitBreaker` GenServer with three-state machine (closed, open, half-open)
- **Sliding window failure tracking** with unified request history
  - Tracks last N requests (both successes and failures) in a single sliding window
  - More accurate than separate windows for each result type
- **Dual threshold strategies**:
  - Absolute threshold: Open circuit after N failures
  - Percentage threshold: Open circuit when failure rate exceeds X%
- **Automatic state transitions**:
  - Closed → Open: When failure threshold is exceeded
  - Open → Half-Open: After timeout period expires
  - Half-Open → Closed: When all test requests succeed
  - Half-Open → Open: When any test request fails
- **Half-open state** with configurable test request limit
- **Manual circuit control**:
  - `HTTPower.CircuitBreaker.open_circuit/1` - Manually open a circuit
  - `HTTPower.CircuitBreaker.close_circuit/1` - Manually close a circuit
  - `HTTPower.CircuitBreaker.reset_circuit/1` - Reset circuit to initial state
  - `HTTPower.CircuitBreaker.get_state/1` - Check current circuit state
- **Flexible circuit breaker configuration**:
  - Global configuration via `config :httpower, :circuit_breaker`
  - Per-client configuration via `HTTPower.new/1`
  - Per-request configuration via request options
  - Custom circuit keys for grouping requests
- **Integration with existing retry logic**: Circuit breaker complements exponential backoff
  - Retry logic handles transient failures (timeouts, temporary errors)
  - Circuit breaker handles persistent failures (service outages, deployment issues)
- Comprehensive test suite (26 new tests covering all states, thresholds, transitions)
- Added circuit breaker section to README with examples and best practices

### Changed

- Refactored circuit breaker sliding window implementation
  - Changed from separate success/failure windows to unified request tracking
  - Uses tuples `{:success | :failure, timestamp}` for better accuracy
  - Window size now correctly limits total requests tracked (not per-type)
- Updated `HTTPower.Client` to integrate circuit breaker into request flow
- Circuit breaker wraps retry logic to provide fail-fast behavior when circuit is open
- Updated documentation to clarify relationship between circuit breaker and retry logic

### Technical Details

- Circuit breaker uses ETS for thread-safe state storage
- State transitions are logged for observability
- Circuit keys default to URL host but can be customized
- Sliding window implementation ensures accurate failure rate tracking
- Half-open state prevents thundering herd by limiting concurrent test requests

## [0.4.0] - 2025-09-30

### Added

- **Built-in rate limiting** with token bucket algorithm
- `HTTPower.RateLimiter` GenServer for managing rate limit state
- `HTTPower.Application` supervision tree for fault tolerance
- **Two rate limiting strategies**:
  - `:wait` - Blocks until tokens are available (up to `max_wait_time`)
  - `:error` - Returns `{:error, :too_many_requests}` immediately
- **Flexible rate limit configuration**:
  - Global configuration via `config :httpower, :rate_limit`
  - Per-client configuration via `HTTPower.new/1`
  - Per-request configuration via request options
  - Custom bucket keys for grouping requests
- **ETS-based storage** for high performance and low latency
- **Automatic bucket cleanup** removes inactive buckets after 5 minutes
- **Time window support**: `:second`, `:minute`, `:hour`
- Thread-safe rate limiting with atomic ETS operations
- Comprehensive test suite (23 new tests covering token bucket algorithm, strategies, concurrent access)

### Changed

- Updated `HTTPower.Client` to check rate limits before each request
- Rate limiting integrated into request flow (happens before logging)
- Default bucket key uses URL host (can be overridden with `:rate_limit_key`)

### Fixed

- Fixed Plug.Conn undefined warnings by adding `@compile {:no_warn_undefined}` directive to `HTTPower.Test`

### Technical Details

- Token bucket algorithm: tokens refill continuously at configured rate
- Refill rate calculated as: `max_tokens / time_window_ms`
- Elapsed time since last refill determines available tokens
- GenServer handles concurrent access with ETS atomic operations
- Cleanup runs every 60 seconds, removes buckets inactive for 5+ minutes
- Works consistently across all adapters (Req, Tesla)

## [0.3.1] - 2025-09-30

### Added

- **PCI-compliant HTTP request/response logging** with automatic data sanitization
- `HTTPower.Logger` module for production-ready logging with security built-in
- **Correlation IDs** for distributed tracing - every request gets a unique ID (format: `req_abc123...`)
- **Request duration tracking** - logs include timing information in milliseconds
- **Automatic sanitization** of sensitive data in logs:
  - Credit card numbers (13-19 digits with optional spaces/dashes)
  - CVV codes (3-4 digits)
  - Authorization headers (Bearer tokens, Basic auth)
  - API keys and secret tokens
  - Password fields in JSON bodies
  - Configurable custom fields via application config
- **Configurable logging** via application config:
  - `enabled` - Enable/disable logging globally (default: true)
  - `level` - Log level: :debug, :info, :warning, :error (default: :info)
  - `sanitize_headers` - Additional headers to sanitize
  - `sanitize_body_fields` - Additional body fields to sanitize
- Comprehensive test suite for logging (42 new tests, 98.67% module coverage)
- Logging works consistently across all adapters (Req, Tesla)

### Changed

- Updated `HTTPower.Client` to integrate logging at request/response boundaries
- Request flow now includes: correlation ID generation → request logging → execution → response/error logging
- All HTTP operations now automatically log with sanitization (can be disabled via config)

### Technical Details

- Correlation IDs generated using cryptographically secure random bytes
- Sanitization uses regex patterns for credit cards, CVV codes
- JSON field sanitization supports nested maps and arrays
- Headers normalized to lowercase for consistent sanitization
- Large response bodies (>500 chars) are truncated in logs
- Logging sits above adapter layer - works identically with Req or Tesla

## [0.3.0] - 2025-09-30

### Added

- **Adapter pattern** supporting multiple HTTP clients (Req and Tesla)
- `HTTPower.Adapter` behavior for implementing custom adapters
- `HTTPower.Adapter.Req` - adapter using Req HTTP client
- `HTTPower.Adapter.Tesla` - adapter for existing Tesla users
- **`HTTPower.Test`** - adapter-agnostic testing module with zero external dependencies
  - `HTTPower.Test.setup/0` - enables mocking for current test
  - `HTTPower.Test.stub/1` - registers stub function to handle requests
  - `HTTPower.Test.json/2`, `html/2`, `text/2` - response helpers
  - `HTTPower.Test.transport_error/2` - simulates network failures (timeout, connection errors, etc.)
- `HTTPower.TestInterceptor` - clean separation of test logic from production code using compile-time checks
- Comprehensive test suite proving adapter independence (50 tests total)
- `adapter` option for specifying which adapter to use
- **Smart adapter detection** - automatically uses available adapter (Req preferred if both present)
- Both Req and Tesla are now optional dependencies - choose the one you need
- Example files moved to `docs/` directory

### Fixed

- **Critical**: Fixed double retry bug where Req's built-in retry ran alongside HTTPower's retry
- **Critical**: Fixed double error wrapping where `HTTPower.Error` structs were wrapped multiple times
- Req adapter now explicitly sets `retry: false` to disable Req's retry mechanism
- Retry logic now runs consistently once per request attempt
- Error handling now checks if errors are already wrapped before wrapping again

### Changed

- Internal architecture refactored to use adapter pattern (public API unchanged)
- `HTTPower.Client` now routes requests through adapters instead of calling Req directly
- Design principle updated from "Req-Based" to "Adapter-Based"
- Documentation updated to emphasize adapter flexibility and production features
- Refactored control flow to eliminate nested conditionals (removed `if` inside `with`, `case` inside `if`)
- Main test suite now uses `HTTPower.Test` instead of `Req.Test` for true adapter independence

### Technical Details

- Adapter abstraction allows production features (retry, circuit breaker, rate limiting) to work consistently across HTTP clients
- **Symmetric dependencies**: Both Req and Tesla are optional - install only what you need
- Smart adapter detection automatically selects available adapter (prefers Req if both present)
- Backward compatible: existing code continues to work (Req auto-detected if installed)
- Tesla users can now adopt HTTPower without pulling in Req dependency
- Clear error message if neither adapter is installed
- Test-mode blocking works seamlessly across both adapters
- All 50 tests passing (29 main + 9 Req adapter + 9 Tesla adapter + 3 transport error tests)

## [0.2.0] - 2025-01-09

### Added

- Client configuration with `HTTPower.new/1` for reusable HTTP clients
- Base URL support for configured clients with automatic path resolution
- Option merging between client defaults and per-request settings
- Support for all HTTP methods (GET, POST, PUT, DELETE) with client instances
- Comprehensive test coverage for client configuration functionality
- HTTP status code retry logic following industry standards (408, 429, 500, 502, 503, 504)
- Exponential backoff with jitter for intelligent retry timing
- Configurable retry parameters: `base_delay`, `max_delay`, `jitter_factor`
- Fast unit tests for retry decision logic separated from execution

### Improved

- Retry test suite performance improved by 70% (48s → 15s) through separation of concerns
- Refactored retry decision functions for better testability and maintainability

### Fixed

- Corrected changelog to accurately reflect implemented vs planned features
- Updated documentation to clarify current capabilities

## [0.1.0] - 2025-09-09

### Added

- Basic HTTP methods (GET, POST, PUT, DELETE) with clean API
- Test mode request blocking to prevent real HTTP requests during testing
- Req.Test integration for controlled HTTP testing and mocking
- Smart retry logic with configurable policies and error categorization
- Clean error handling that never raises exceptions
- SSL certificate verification with configurable options
- Proxy configuration support (system proxy and custom proxy settings)
- Request timeout management with sensible defaults
- Comprehensive error messages for common network issues
- Support for custom headers and request bodies
- Automatic Content-Type headers for POST requests
- Connection close headers for reliable request handling
- Mint.TransportError handling for detailed network error reporting
- Complete test suite with 100% coverage
- Full documentation with examples and API reference
- Hex package configuration for easy installation

### Technical Details

- Built on top of Req HTTP client for reliability
- Uses Mint for low-level HTTP transport (via Req)
- Supports HTTP/1.1 and HTTP/2 protocols
- Elixir 1.14+ compatibility
- Production-ready error handling and logging
- PCI DSS compliance considerations in design

[0.23.0]: https://github.com/mdepolli/httpower/compare/v0.22.0...v0.23.0
[0.22.0]: https://github.com/mdepolli/httpower/compare/v0.21.0...v0.22.0
[0.21.0]: https://github.com/mdepolli/httpower/compare/v0.20.0...v0.21.0
[0.20.0]: https://github.com/mdepolli/httpower/compare/v0.19.0...v0.20.0
[0.19.0]: https://github.com/mdepolli/httpower/compare/v0.18.0...v0.19.0
[0.18.0]: https://github.com/mdepolli/httpower/compare/v0.17.0...v0.18.0
[0.17.0]: https://github.com/mdepolli/httpower/compare/v0.16.0...v0.17.0
[0.16.0]: https://github.com/mdepolli/httpower/compare/v0.15.2...v0.16.0
[0.15.2]: https://github.com/mdepolli/httpower/compare/v0.15.1...v0.15.2
[0.15.1]: https://github.com/mdepolli/httpower/compare/v0.15.0...v0.15.1
[0.15.0]: https://github.com/mdepolli/httpower/compare/v0.14.0...v0.15.0
[0.14.0]: https://github.com/mdepolli/httpower/compare/v0.13.0...v0.14.0
[0.13.0]: https://github.com/mdepolli/httpower/compare/v0.12.0...v0.13.0
[0.12.0]: https://github.com/mdepolli/httpower/compare/v0.11.0...v0.12.0
[0.11.0]: https://github.com/mdepolli/httpower/compare/v0.10.0...v0.11.0
[0.10.0]: https://github.com/mdepolli/httpower/compare/v0.9.0...v0.10.0
[0.9.0]: https://github.com/mdepolli/httpower/compare/v0.8.1...v0.9.0
[0.8.1]: https://github.com/mdepolli/httpower/compare/v0.8.0...v0.8.1
[0.8.0]: https://github.com/mdepolli/httpower/compare/v0.7.1...v0.8.0
[0.7.1]: https://github.com/mdepolli/httpower/compare/v0.7.0...v0.7.1
[0.7.0]: https://github.com/mdepolli/httpower/compare/v0.6.0...v0.7.0
[0.6.0]: https://github.com/mdepolli/httpower/compare/v0.5.0...v0.6.0
[0.5.0]: https://github.com/mdepolli/httpower/compare/v0.4.0...v0.5.0
[0.4.0]: https://github.com/mdepolli/httpower/compare/v0.3.1...v0.4.0
[0.3.1]: https://github.com/mdepolli/httpower/compare/v0.3.0...v0.3.1
[0.3.0]: https://github.com/mdepolli/httpower/compare/v0.2.0...v0.3.0
[0.2.0]: https://github.com/mdepolli/httpower/compare/v0.1.0...v0.2.0
[0.1.0]: https://github.com/mdepolli/httpower/releases/tag/v0.1.0