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

The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.

[Unreleased]

0.23.1 - 2026-06-29

Security

  • Bumped vulnerable dev/test lockfile dependenciesmix.lock pinned mint 1.7.1, req 0.5.15, and tesla 1.15.3, all flagged by mix hex.audit/deps.audit (HTTP/2 floods and HTTP/1 response smuggling in Mint; decompression-bomb DoS and multipart header injection in Req; cross-origin auth-header leak, atom-table exhaustion, and decompression bomb in Tesla). Updated to mint 1.9.0, req 0.6.2, tesla 1.20.0, and finch 0.23.0. These are optional/transitive dependencies; HTTPower's mix.exs already declares them with open >= constraints, so consuming apps resolve patched versions on their own — this bump clears the advisories for HTTPower's own CI and lockfile.

  • SSRF guards now cover redirects (fail closed) — The :block_private_ips and :allowed_hosts guards validate only the initial request URL. The Req adapter follows redirects automatically, so a permitted host could 30x-redirect to a private or disallowed target (e.g. 169.254.169.254) that was never re-validated. When either guard is active, HTTPower now disables Req's automatic redirect following, returning the 3xx response as-is instead of transparently following it. The Finch adapter never auto-follows redirects (already safe); the Tesla adapter only follows when the user adds Tesla.Middleware.FollowRedirects to their own client. Internally, HTTPower.Client passes a :block_redirects flag to the adapter layer when a guard is configured.

Fixed

  • Finch adapter: unwrap Finch.TransportError to the bare reason atom — Finch 0.21+ wraps transport failures in a Finch.TransportError struct (carrying the underlying Mint error in :source), whereas older versions surfaced a bare Mint.TransportError. The adapter only unwrapped the latter, so connection errors (e.g. :econnrefused) leaked through as an unmatched struct instead of the reason atom HTTPower.Retry matches on — breaking transport-error retry classification. The adapter now unwraps both struct shapes.

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 GenServercheck_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 pathconsume/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 namespacesanitize_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:

    # 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 falsededuplicate/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

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: :systemproxy: :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 adaptersconvert_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 proxymaybe_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 probesmaybe_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 eventHTTPower.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 URLssanitize_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 timeget_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 disabledHTTPower.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 RateLimiterclear_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 dedupDedup.deduplicate/2 no longer uses :infinity timeout. The default 5s timeout surfaces a stuck GenServer instead of hanging indefinitely.

  • Guard Logger against non-map headerssanitize_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-onemax_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 bodyHTTPower.delete/2 now extracts and forwards the :body option, matching the behavior of POST, PUT, and PATCH.

  • Dedup wait telemetry reports actual wait durationwait_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 adaptermaybe_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.FeatureHTTPower.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:

# 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:

# 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/3execute_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.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