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 dependencies —
mix.lockpinnedmint1.7.1,req0.5.15, andtesla1.15.3, all flagged bymix 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 tomint1.9.0,req0.6.2,tesla1.20.0, andfinch0.23.0. These are optional/transitive dependencies; HTTPower'smix.exsalready 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_ipsand:allowed_hostsguards 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 addsTesla.Middleware.FollowRedirectsto their own client. Internally,HTTPower.Clientpasses a:block_redirectsflag to the adapter layer when a guard is configured.
Fixed
- Finch adapter: unwrap
Finch.TransportErrorto the bare reason atom — Finch 0.21+ wraps transport failures in aFinch.TransportErrorstruct (carrying the underlying Mint error in:source), whereas older versions surfaced a bareMint.TransportError. The adapter only unwrapped the latter, so connection errors (e.g.:econnrefused) leaked through as an unmatched struct instead of the reason atomHTTPower.Retrymatches on — breaking transport-error retry classification. The adapter now unwraps both struct shapes.
0.23.0 - 2026-06-28
Added
HTTPower.Sanitizermodule — Extracted the PCI-compliant header/body sanitization logic out ofHTTPower.Loggerinto a neutral, dedicated module. It is now the single source of truth, used by bothHTTPower.Client(for telemetry metadata) andHTTPower.Logger(for log output).HTTPower.Logger.sanitize_headers/1andHTTPower.Logger.sanitize_body/1remain available as delegations for backward compatibility.pool_timeoutoption 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_ipsand:allowed_hostsoptions — Opt-in egress controls validated before any HTTP call.block_private_ips: truerejects requests whose host is a loopback, RFC1918 private, link-local (incl. the169.254.169.254cloud-metadata address), or IPv6 unique-local/loopback literal, plus thelocalhosthostname, 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 viaconfig :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/6is nowexecute_with_retry/2(with an optional thirdtelemetry_metadataargument). It now takes a 0-arityexecute_fnclosure plus the requestopts, instead of(method, url, body, headers, adapter, opts). This breaks the circular dependency betweenHTTPower.RetryandHTTPower.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/2is nowprepare_headers/1— themethodargument was never used. Affects only custom adapter authors who called this public helper directly.Finch adapter: removed the non-functional per-request
ssl_verify/proxyhandling — these options never took effect with the Finch adapter and were silently discarded: Finch configures TLS and proxy at the pool level (baked in atFinch.start_link), andFinch.request/3has 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 viaconfig :httpower, :finch_pools(which flows into Finch's:pools). Without explicit TLS config the pool inherits Mint's defaultverify: :verify_peer, so certificates are still verified by default. The Req adapter continues to honorssl_verify/proxyper request (it starts a Finch pool per distinctconnect_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
finchdependency (>= 0.19.0), but Finch 0.22.0+ requires Elixir~> 1.15. The previous~> 1.14floor 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 becausemix.lockpinned 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/2now serves the common case (a closed or not-yet-created circuit) from a direct ETS read instead of a serializingGenServer.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/2previously serialized every request through a singleGenServer.call(which also made the ETS table'swrite_concurrencymoot, 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-requestApplication.get_envlookup was also removed from the hot path (the global:rate_limitdefault is now read lazily, only for keys a request's config omits). Rate-limiting semantics (burst capacity, refill rate,:wait/:errorstrategies, 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 withfailure_count/success_countmaintained incrementally, so recording andshould_open?are O(1) regardless ofwindow_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/1now returns the raw GCRAtat(an integer, monotonic µs) ornil, instead of a{tokens, last_refill_ms}tuple;RateLimiter.get_info/1now returns%{tat_us: integer}instead of%{current_tokens:, last_refill_ms:}; andRateLimiter.update_from_headers/2is nowupdate_from_headers/3, taking the rateconfig(needed to position the timestamp from the server's reported remaining count). For a human-meaningful "tokens remaining" value, usecheck_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
:loggingto a dedicated:sanitizationnamespace —sanitize_headersandsanitize_body_fieldsare now read fromconfig :httpower, :sanitizationinstead ofconfig :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
:loggingoptions (level,log_headers,log_body) are unchanged, as are the:sanitize_headers/:sanitize_body_fieldsoptions passed directly toHTTPower.Logger.attach/1.Internal
HTTPower.Middleware.Dedupfunctions are now@doc false—deduplicate/2,complete/3,cancel/1, andhash/3operate on the internal SHA256 request hash and were never part of the public surface, but appeared in the generated docs (with staleHTTPower.RequestDeduplicatorexamples). They are now hidden. No behavior or signature change.Rate limiter
:waitstrategy 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, andmax_wait_timeaccounting is unchanged (it tracks the base wait, not the jittered sleep).
Deprecated
HTTPower.Logger.sanitize_headers/1andHTTPower.Logger.sanitize_body/1— Now delegate toHTTPower.Sanitizerand emit a deprecation warning when called. UseHTTPower.Sanitizer.sanitize_headers/1andHTTPower.Sanitizer.sanitize_body/1instead. The delegations will be removed in a future release.
Fixed
HTTPower.Error.message/1now renders{:feature_error, module, reason}as a readable message —"Middleware <Module> failed: <reason>"instead of falling through toinspect/1and 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.Teslainstead of the{module, tesla_client}tuple) raised anArgumentError, breaking HTTPower's "never raises" contract. It now returns{:error, %HTTPower.Error{reason: :missing_tesla_client}}like every other error path.Adapter
catchbranches now unwrap transport errors like therescuebranch — A thrown (rather than returned or raised)%Mint.TransportError{}/%Req.TransportError{}left thecatchclause in the Finch, Req, and Tesla adapters returning the wrapped struct instead of the bare reason atom, soHTTPower.Retrycould not classify it as a retryable transport error. Thecatchclause now appliesunwrap_transport_error/1, matching therescueclause.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, soHTTPower.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) raisedCaseClauseError{term: {:ok, :system}}: Req forwardsconnect_options[:proxy]toMint.HTTP.connect/4, which only accepts a{scheme, address, port, opts}tuple. Neither Mint nor Req has system-proxy auto-detection, so:systemis 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
nilbody to"", which emitsContent-Length: 0on bodyless requests such as GET (RFC 9110 §9.3.1 says a GET should not include a body, and some servers reject it).nilis now passed through toFinch.build/4andTesla.Env, both of which treat it as "no body".Req adapter response headers are now consistently list-valued across adapters —
convert_responseusedMap.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 viaHTTPower.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_optionsonly forwardedproxyvalues 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'sconnect_options[:proxy].Circuit breaker no longer closes early from half-open when configured for multiple probes —
maybe_transition_from_half_open_to_closedcounted window-wide successes, which still included successes recorded before the trip. Withhalf_open_requests > 1, a single successful probe could already satisfy the threshold and close the circuit prematurely (masked by the default of1). 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 readmeasurements.durationdirectly with no error handling, and:telemetrypermanently 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_countin[:httpower, :request, :stop]telemetry now reflects actual retries — the metadata read a:retry_countoption that was never written, so it was always0.HTTPower.Retry.execute_with_retrynow returns{result, retry_count}andHTTPower.Clientthreads the real count into the stop metadata.can_do_request?guards theHTTPower.Testlookup — whentest_mode: true, the test-mode check calledHTTPower.Test.mock_enabled?/0unguarded, which would raise if the test module were not loaded. It is now wrapped inCode.ensure_loaded?(HTTPower.Test), mirroringHTTPower.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\nand inject additional headers or split the request (CRLF injection).HTTPower.Clientnow 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 wiredmix deps.audit(known CVEs) plusmix hex.audit(retired packages) into the CIdeps_checkjob, so vulnerable or retired dependencies fail the build. The first run surfaced a real issue: the lockedplugwas on a version affected by a high-severity multipart-header DoS advisory (GHSA-468c-vq7p-gh64);mix.lockis bumped to a patchedplugrelease.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 insideHTTPower.Logger's own handler, so every other telemetry consumer (Datadog/OpenTelemetry exporters, custom handlers) received credit card numbers, CVVs,Authorizationheaders,Set-Cookie, and full bodies in the clear. Sanitization now happens at the emission boundary inHTTPower.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/cidandcard_cvv/security-code(the digit-count constraint keeps ambiguous keywords likecidfrom 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/1already 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 (Stripesk_/pk_/rk_/whsec_, GitHubghp_/github_pat_/…, Slackxox*-, AWSAKIA…) 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-resetvalue (beyond year 9999) madeHTTPower.RateLimitHeaders.parse/2raise viaDateTime.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 — Passparams: [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 — Passjson: %{key: value}to encode the body as JSON, setContent-Type: application/json, and setAccept: application/jsonin one step. Replaces the previous pattern of manually callingJason.encode!and setting headers.form:option for automatic form-urlencoded request body encoding — Passform: %{key: value}to encode the body as URL-encoded form data and setContent-Type: application/x-www-form-urlencodedautomatically.raw:option to skip automatic response body decoding — Passraw: trueto receive the raw binary response body, bypassing Content-Type-driven decoding.HTTPower.Codecmodule for adapter-independent body encoding/decoding — Centralizes request encoding (json:,form:) and Content-Type-driven response decoding (auto-decodesapplication/jsonand+jsonsuffix responses). Lives above the adapter layer; called fromHTTPower.Client.New error reasons
:conflicting_body_optionsand:json_encode_error—:conflicting_body_optionsis returned when more than one ofjson:,form:, orbody:is provided for the same request.:json_encode_erroris returned whenjson: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 newform:option or set the header explicitly. This default was added in v0.1.0 but conflicted with the newjson:andform:encoding options and was inconsistent with other HTTP methods. Applications that relied on this default must addform: paramsor 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-Typeheader indicates JSON.BREAKING: Tesla users with
Tesla.Middleware.JSONmust remove it — Having bothTesla.Middleware.JSONandHTTPower.Codecin the stack results in double-decoding of JSON responses.BREAKING: Telemetry
request.bodymetadata contains encoded JSON string whenjson:is used — When using thejson: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 reflectsURI.t()instead ofString.t().call_adapternormalizes strings to URI as a safety net. Finch and Req acceptURI.t()natively; Tesla converts internally.Cache rate limiter default config at compile time —
get_strategy/1andget_max_wait_time/1now use@default_configviaApplication.compile_envinstead of callingApplication.get_envper request.Move
@cleanup_intervalto top of Dedup module — Grouped with other module attributes for consistency.Remove no-op pipeline order test — The test in
coordination_test.exsasserted 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.Codecand is driven by theContent-Typeresponse header.Req adapter's built-in response decoding is disabled —
HTTPower.Codechandles 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_unavailableatom, matchinghandle_request/2behavior.Remove rescue-for-control-flow in RateLimiter —
clear_adaptive_state/1no longer usesrescue ArgumentError -> :ok.:ets.delete/2on 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/2whenmax_retriesis set to high values.2^30(~1 billion ms) exceeds any practicalmax_delay.Use default GenServer timeout for dedup —
Dedup.deduplicate/2no longer uses:infinitytimeout. 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 onmap_size/1.
0.20.0 - 2026-03-08
Added
- Finch adapter unit tests — Added 27 unit tests for
HTTPower.Adapter.Finchcovering HTTP methods, headers, SSL, proxy, response conversion, error handling, body handling, andconn_optsmerging. Finch was the only adapter without unit tests.
Changed
Extract shared
prepare_headers/2intoHTTPower.Adapter— Consolidated duplicate header preparation logic from Finch, Req, and Test modules into a single public function in theHTTPower.Adapterbehaviour module.Cache sanitization field lists in Logger at attach time — Sanitization header and body field lists are now normalized once during
HTTPower.Logger.attach/0and stored in the handler config, eliminating repeatedApplication.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/1raises on invalid profile — Clarified that the "never raises" principle applies to HTTP operations, not configuration errors.new/1raisesArgumentErrorfor unknown profiles, which is correct Elixir convention.Bump ExDoc to
~> 0.40— Enables automaticllms.txtgeneration 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 withUndefinedFunctionError.
Fixed
Dedup
pid_to_hashoverwrite when process tracks multiple hashes — Changed from a single hash per PID to aMapSetof hashes. Previously, if a process made concurrent requests with different bodies, only the last hash was tracked, leaking earlier entries on process death.max_retriesoff-by-one —max_retries: 3now 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/2now extracts and forwards the:bodyoption, matching the behavior of POST, PUT, and PATCH.Dedup wait telemetry reports actual wait duration —
wait_time_msin[:httpower, :dedup, :wait]events now reflects the actual time spent waiting for the original request, instead of always reporting 0.circuit_breaker_keyworks as top-level request option — Previously only recognized when nested insidecircuit_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 inClient.get_request_function/2telemetry metadata that could never match, since the retry module converts HTTP status errors to{:ok, response}.parse_retry_after/1normalizes header keys — Now performs case-insensitive header lookup, matching the behavior ofparse/2. Previously, mixed-case keys like"Retry-After"would not be found when callingparse_retry_after/1directly.
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.debugfrom 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/3now 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_thresholdin profiles — Configuration profiles now setfailure_thresholdexplicitly 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$callerschain walking. For pre-existing processes (GenServers, Agents), use the newHTTPower.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.Errormodule - Added unit tests for error struct creation and theerror_message/1helper function.Configurable dedup wait timeout - The dedup
wait_timeoutoption is now user-configurable via thededuplicatekeyword list (e.g.,deduplicate: [wait_timeout: 30_000]).
Changed
Elixir 1.20.0-rc.2 - Updated
.tool-versionsto 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
:telemetryas 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/2directly 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_requesttreated 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 withstatus >= 500orstatus == 429are correctly recorded as failures.Rate limiter
:waitstrategy re-checks tokens after sleeping - Previously, the:waitstrategy 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_infocatch-all in RateLimiter and Dedup GenServers - Added catch-allhandle_info/2clauses to prevent crashes from unexpected messages.Error.reasontype spec includes tuple variants - Fixed the typespec to reflect thatreasoncan be a tuple (e.g.,{:circuit_breaker, :open}), not just an atom.Removed unreachable
error_message/1catch-all clause - Cleaned up dead code in the Error module.Client uses
request.headersinstead 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/2previously used two separate GenServer calls (check_rate_limitthenconsume_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 sharedrefill_bucket/3helper to reduce duplication betweencheck_rate_limitandcheck_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_optionsandmaybe_add_proxy_optionsnow merge into existing:connect_optionsinstead 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. TheResponse.t()type spec is updated to%{optional(String.t()) => [String.t()]}.Hardcoded
connection: closeheader removed - Finch and Req adapters no longer injectconnection: closeon 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 produceshttps://api.com/usersinstead ofhttps://api.com//users.Telemetry
circuit_key: "unknown"fallback removed -emit_state_change_eventnow 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, andHTTPower.options/2to the public API, with full client instance support.HTTPower.Configmodule - 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: :testtooptional: trueso library consumers can useHTTPower.Testwithout 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 -
HTTPowerandHTTPower.Adaptermoduledocs now correctly describe the adapter-based architecture with Finch as the default.Mint.TransportError decoupled from core - Adapters now unwrap
Mint.TransportErrorstructs to plain atoms at the adapter boundary.HTTPower.ErrorandHTTPower.Retryno 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_optionsnow usesKeyword.keyword?/1instead 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/1now 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
:telemetryperformance warnings. - Code simplification - Consolidated duplicate
error_message/1intoError.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.20to>= 0.19.0(optional) - Changed Req from
~> 0.4.0to>= 0.4.0(optional) - Changed Tesla from
~> 1.11to>= 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
- Changed Finch from
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.TestInterceptorto use runtime checks instead of compile-timeMix.env()check - Test interception now works regardless of how HTTPower was compiled (hex package or local path)
- Changed from
if Mix.env() == :testto runtimeCode.ensure_loaded?(HTTPower.Test) and HTTPower.Test.mock_enabled?() - Enables
HTTPower.Test.stub/1to work in consuming applications that install HTTPower from Hex - Previously, test mocking only worked when HTTPower was compiled locally in test environment
- Fixed
Technical Details
TestInterceptor.test_enabled?/0now checks ifHTTPower.Testmodule 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
- Wrapped all adapter modules (Finch, Req, Tesla) with
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 usesCode.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_limitmeasurement +:rate_limit_bypassin 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: truein 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 profilesHTTPower.Profiles.get/1- Get profile configuration- Profiles support option overrides with deep merge (nested options preserved)
- Dedup bypasses rate limiting - Cache hits skip rate limiting entirely (5x effective capacity improvement)
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/1functions (replaced with direct pattern matching) - Cleaner error handling for middleware failures
- Fixed pipeline short-circuit bug where
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]preservesper: :minutefrom profile - Prevents accidental override of entire nested config
- Explicit options always win over profile defaults
- User options now deep merge with profile defaults for nested keys (
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
withstatement tocasestatement for clearer flow control - Direct pattern matching on
{:ok, %Request{}}vs{:halt, %Response{}} - Post-request handlers only called when HTTP actually executed
- Changed from
- 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.TesttoHTTPower.Test(adapter-agnostic) - All 51 logger tests now test with Finch adapter
- Tests demonstrate adapter-agnostic testing approach
- Converted from
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.Featurebehaviour for composable request pipeline features - New
HTTPower.Requeststruct 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.privatemap
- Introduced
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.hostinstead 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.Featurebehaviour - Simplified key extraction:
request.url.hostinstead of URL parsing - Features store state in
request.privatefor post-request processing - Circuit breaker and dedup can short-circuit pipeline with
{:halt, response} - More consistent error handling across all features
- All features (RateLimiter, CircuitBreaker, Dedup) now implement
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.Featurebehaviour - 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_headersandlog_bodyconfiguration - Large bodies automatically truncated to 500 characters in metadata
- All sensitive data sanitized before adding to metadata
- Added 9 comprehensive tests for metadata functionality
- All log entries now include structured metadata via
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
- Added
Performance: CircuitBreaker async recording
- Switched from synchronous
GenServer.callto asyncGenServer.castfor 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
- Switched from synchronous
Performance: Configuration caching optimization
- Implemented compile-time config caching using
Application.compile_env - Eliminates repeated
Application.get_envcalls 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
- Implemented compile-time config caching using
Technical Details
- ETS concurrency:
{:write_concurrency, true}uses distributed locks for better parallelism - CircuitBreaker: Test updates include
await_state/3helper 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
:telemetrylibrary- 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 (
:telemetryships with Elixir) - Full observability guide with Prometheus, OpenTelemetry, and LiveDashboard examples
- HTTP request lifecycle events:
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_requestsand: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
- Changed
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}} # NEW0.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_requestslimit - Increment counter BEFORE allowing request through (prevents concurrent bypass)
- Added comprehensive concurrent request test (10 concurrent requests, verifies only 3 allowed)
- Fixed race condition where multiple concurrent processes could exceed
- 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_envcalls) - ~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
- Cache default configuration at GenServer startup (eliminates repeated
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
:DOWNmessage 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.DedupGenServer 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: trueordeduplicate: [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-Afterheader (integer seconds format)
- GitHub/Twitter style:
- Auto-detection with
:autoformat (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 headersHTTPower.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-Aftersupported since v0.15.2 (previously only integer seconds) - Automatic backoff - Retry logic respects
Retry-Afterheader on 429/503 responses- When server provides
Retry-Afterheader (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
- When server provides
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
withpipelines 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/3for clarity - Removed
do_request/3wrapper that just delegated toexecute_http_request/3
0.6.0 - 2025-09-30
Added
- Global adapter configuration - Configure HTTP adapter application-wide
config :httpower, adapter: HTTPower.Adapter.Reqfor global adapter selectionconfig :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/directoryguides/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 differencesguides/configuration-reference.md- Complete option reference with availability matrixguides/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_headersandsanitize_body_fields(not nested undersanitize:) - Clarified that custom sanitization fields are additive (supplement defaults, not replace)
- Updated all examples to use correct configuration structure
- Fixed sanitization config 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.CircuitBreakerGenServer 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 circuitHTTPower.CircuitBreaker.close_circuit/1- Manually close a circuitHTTPower.CircuitBreaker.reset_circuit/1- Reset circuit to initial stateHTTPower.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
- Global configuration via
- 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.Clientto 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.RateLimiterGenServer for managing rate limit stateHTTPower.Applicationsupervision tree for fault tolerance- Two rate limiting strategies:
:wait- Blocks until tokens are available (up tomax_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
- Global configuration via
- 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.Clientto 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 toHTTPower.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.Loggermodule 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 sanitizesanitize_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.Clientto 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.Adapterbehavior for implementing custom adaptersHTTPower.Adapter.Req- adapter using Req HTTP clientHTTPower.Adapter.Tesla- adapter for existing Tesla usersHTTPower.Test- adapter-agnostic testing module with zero external dependenciesHTTPower.Test.setup/0- enables mocking for current testHTTPower.Test.stub/1- registers stub function to handle requestsHTTPower.Test.json/2,html/2,text/2- response helpersHTTPower.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)
adapteroption 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.Errorstructs were wrapped multiple times - Req adapter now explicitly sets
retry: falseto 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.Clientnow 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
ifinsidewith,caseinsideif) - Main test suite now uses
HTTPower.Testinstead ofReq.Testfor 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/1for 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