fix: deep-audit correctness + public-API findings#64
Merged
Conversation
2.0 ** attempt_index raises OverflowError in Python for attempt_index >= 1024 rather than saturating to math.inf as the old docstring claimed. Catch the OverflowError and clamp the ceiling directly to max_delay, which is the correct clamped result. Tests added to pin both the no-raise and the exact max_delay clamp behaviours. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
When a server's Retry-After header exceeds max_delay, the middleware previously consumed a budget token before giving up, allowing a Retry-After flood to silently drain shared-budget capacity. Move the retry_after > max_delay guard before try_withdraw() in both AsyncRetry and Retry so the token is only spent when a real retry will happen. Adds test_retry_after_exceeds_max_delay_does_not_consume_budget_token (async) and …_sync (sync) to pin the invariant. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
For URLs like `http://user:pass@/path` (credentials but no hostname), the previous reconstruction from `parts.hostname` yielded an empty netloc, causing `urlunsplit` to emit `http:///path`. Fix by slicing the `user:pass@` prefix directly off `parts.netloc`; when the remainder is empty (no real host), reconstruct without an authority section to produce `http:/path` instead. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…spec absent When msgspec is not installed, `_contains_custom_type` would raise `NameError: name 'msgspec' is not defined` rather than the friendly `ImportError`. Added an early guard that raises `ImportError(MISSING_DEPENDENCY_MESSAGE)` when `import_checker.is_msgspec_installed` is False, matching the pattern used in `MsgspecDecoder.__init__`. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…g caveat The module docstring said "asyncio-safe" without noting that a sync thread holding the threading.Lock can briefly block the event-loop thread. Added clarification that the critical section is intentionally tiny to bound latency, while preserving the core safety guarantee (no torn state). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Factor `_is_replayable_type` shared helper so both async and sync detectors use the same exclusion list. `_is_streaming_body_async` now returns True when the body has `__iter__` and is not a replayable type (bytes/bytearray/memoryview/str/dict/list/tuple), closing the gap where a sync generator passed to AsyncClient bypassed the streaming-body marker and the AsyncRetry replay guard. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…r-import leakage Without __all__, `from httpware.middleware import *` would expose Awaitable, Callable, Protocol, TypeAlias, runtime_checkable, httpx2, chain, and resilience. Added explicit __all__ listing exactly the 10 public names (protocols + phase decorators). Tests added to test_public_api.py to guard this contract. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
float(int("9" * 400)) raises OverflowError (not ValueError) in Python,
crashing the retry loop when a server sends a pathologically large
Retry-After integer string. Widen the except clause to
(ValueError, OverflowError) so oversized values fall back to jitter
backoff rather than propagating unhandled.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Follow-up reconciliation after the parallel correctness fixes: - _strip_userinfo's no-host fix only covered the no-query case; _mask_query's urlunsplit re-introduced http:///path when a sensitive query was masked. Factor a shared _reassemble() helper used by both, so the empty-authority triple-slash is avoided everywhere (normal URLs stay byte-identical). - hoist a function-body import in the msgspec test (ruff PLC0415) and use the project's # ty: ignore form. - cover the empty-netloc query/fragment branches, the @-in-path guard, and mark the never-iterated sync-generator test body no-cover. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This was referenced Jun 14, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Closes the confirmed correctness and public-API findings from the 2026-06-14 deep audit that #62/#63 didn't already cover. Each fix lands TDD-first from the audit's reproducer.
Retry-After > max_delaygive-upbudget.try_withdraw()in bothAsyncRetryandRetry_parse_retry_afterOverflowErroron a huge digit string crashes the loopexcept (ValueError, OverflowError)→ degrades to "no hint"full_jitter_delayOverflowErroratattempt_index >= 1024OverflowError, clamp tomax_delay; docstring corrected to match_strip_userinfoemittedhttp:///pathfor creds-but-no-host_reassemble()avoids the empty-authority triple-slash in both_strip_userinfoand_mask_query; normal URLs byte-identical_contains_custom_typeNameErrorwhen msgspec absentImportErrorbefore anymsgspec.*ref_is_streaming_body_asyncignored sync iterables_is_replayable_typehelper; async detector now marks non-replayable sync iterables as streaming (replay guard no longer relies on an undocumented httpx2 detail)RetryBudget"asyncio-safe" docstring lacked the blocking caveatmiddleware/__init__.pyhad no__all__(9+ star-leaks)__all__of the ten public names, matching sibling subpackagesNotes
_reassembleunification — the initial_strip_userinfofix didn't cover the masked-query path, which my added test caught).Test plan
just test— 620 passed, 100% coveragejust lint— ruff + ty cleanhttpx2._, no rawstr(request.url)regressions🤖 Generated with Claude Code