Skip to content

fix: dedupe pass-through route.call_count across transport retries (#126)#320

Open
phillza wants to merge 2 commits into
lundberg:masterfrom
phillza:fix/pass-through-call-count-issue-126
Open

fix: dedupe pass-through route.call_count across transport retries (#126)#320
phillza wants to merge 2 commits into
lundberg:masterfrom
phillza:fix/pass-through-call-count-issue-126

Conversation

@phillza

@phillza phillza commented Jun 17, 2026

Copy link
Copy Markdown

Fix pass-through route.call_count overcounting (#126)

Summary

When a pass_through() route is hit, the underlying transport (typically httpcore) may retry a failed network operation by re-invoking the same request through respx. Each invocation flows through Router.resolver, matches the pass-through route, and records a call — so a single user-initiated httpx call that fails and retries internally shows up as call_count == 2.

with respx.mock as mock:
    route = respx.get("https://example.com").pass_through()
    try:
        httpx.get("https://example.com")          # ConnectError
    except httpx.ConnectError:
        pass
    assert route.call_count == 1                  # was 2 before this fix

Root cause

httpcore rebuilds the httpx.Request on retry, so id(request) is not stable across the two invocations. The original Router.record therefore sees two distinct request objects entering the except PassThrough branch and records both.

Fix

Router now keeps a Set[Tuple[str, str, bytes]] of (method, url, body) keys for requests already recorded as pass-through. The dedup key is stable across retries because it is content-based, not identity-based.

Trade-off: two user-initiated calls with identical method, URL, and body would also be deduplicated. This is documented in _pass_through_request_key's docstring. The over-count from a single failed retry is the much more common surprise than two rapid-fire identical GETs.

The set is cleared by Router.reset(), so a fresh respx.mock session starts counting from zero again.

Tests

Three new tests in tests/test_router.py:

  • test_pass_through_dedupes_internal_retries — direct reproducer for Overcounting pass-through requests #126
  • test_pass_through_distinguishes_different_bodies — POSTs with different bodies are not deduplicated
  • test_pass_through_dedup_clears_on_resetreset() clears the dedup set

Full suite: 324 passed, 2 skipped (intentional), 100% coverage maintained.

Files changed

  • respx/router.py — add _pass_through_request_key helper, add _seen_pass_through_request_keys field to Router.__init__, clear it in Router.reset(), use it for dedup in the PassThrough branch of Router.resolver
  • tests/test_router.py — three new tests
  • CHANGELOG.md — entry under [Unreleased]Fixed

Closes #126

phillza added 2 commits June 17, 2026 20:52
When a pass-through route is hit, the underlying transport (typically
httpcore) may retry a failed network operation by re-invoking the same
request through respx. Each invocation flows through Router.resolver,
matches the pass-through route, and records a call - so a single
user-initiated httpx call that fails and retries internally shows
up as call_count == 2.

The original report:
    with respx.mock as mock:
        route = respx.get('https://example.com').pass_through()
        httpx.get('https://example.com')          # fails with ConnectError
        route.call_count                          # was 2, expected 1

httpcore rebuilds httpx.Request on retry, so id(request) is not stable
across the two invocations. This fix deduplicates by (method, url, body)
in Router._seen_pass_through_request_keys, populated by the resolver's
PassThrough branch and cleared by Router.reset().

Two user-initiated calls with the same method/url/body would also be
deduplicated - the trade-off is documented in the helper docstring;
the over-count from a single failed retry is the much more common
surprise than two rapid-fire identical GETs.

Closes lundberg#126
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Overcounting pass-through requests

1 participant