Summary
A token request retried onto a fallback host is re-sent byte-identical — same nonce,
same timestamp, same MAC — so the server rejects the second submission with
40105 401 Nonce value replayed.
This shows up as an intermittent CI failure in restcapability_test.py and
restauth_test.py across branches and Python versions.
Mechanism
Http.make_request serialises the body once, before the host loop:
if body is not None and type(body) not in (bytes, str):
body = self.dump_body(body)
Inside the loop, build_request is rebuilt per host but passed content=body — the same
bytes. On any exception from send, the handler re-raises only if should_stop_retrying()
and otherwise advances to the next host with that identical body.
The nonce is generated per create_token_request call (uuid.uuid4().hex[:16]), so it is
fresh per call but frozen across retries of that call.
The fallback-host loop is the only path that resends. Ruled out: httpx's default transport
is retries=0; httpcore's pool only re-queues on ConnectionNotAvailable, i.e. when the
request was never written; and reauth_if_expired returns early for skip_auth, which
token requests pass.
For the test configuration get_hosts() returns 3 hosts (5 endpoint fallbacks truncated
to Defaults.http_max_retry_count = 3), so up to 2 fallback retries happen.
Why the server rejects it
If the first POST reaches the server and its nonce is consumed, but the response is lost —
for example an HTTP/2 GOAWAY terminating the connection, which is live in the same CI runs
— the retry is a genuine replay. test_request_token_with_duplicate_nonce already asserts
the server rejects a repeated (key, timestamp, nonce).
Reproduction
With a fake endpoint that consumes a nonce when it processes a request, returns 40105 for
any nonce it has already consumed, and drops the response to the first attempt:
|
attempts |
distinct nonces |
result |
| no lost response |
1 |
1 |
OK |
| lost response, current behaviour |
2 |
1 |
40105 401 Nonce value replayed |
| lost response, re-signed per attempt |
3 |
2 |
OK |
Suggested direction
Two options, both changing make_request's contract:
- Pass a body factory so each attempt re-signs. Architecturally right; needs care so the
auth_callback / auth_url paths are not re-invoked, only the key-signed path.
- Retry
40105 at the auth layer with a freshly generated token request. Smaller, leaves
http.py and RSC15 untouched, slightly wasteful if the first submission did succeed.
More generally: any non-idempotent request is resent onto a fallback host after a lost
response. Token requests are just the case where the server detects and rejects it.
🤖 Generated with Claude Code
┆Issue is synchronized with this Jira Task by Unito
Summary
A token request retried onto a fallback host is re-sent byte-identical — same nonce,
same timestamp, same MAC — so the server rejects the second submission with
40105 401 Nonce value replayed.This shows up as an intermittent CI failure in
restcapability_test.pyandrestauth_test.pyacross branches and Python versions.Mechanism
Http.make_requestserialises the body once, before the host loop:Inside the loop,
build_requestis rebuilt per host but passedcontent=body— the samebytes. On any exception from
send, the handler re-raises only ifshould_stop_retrying()and otherwise advances to the next host with that identical body.
The nonce is generated per
create_token_requestcall (uuid.uuid4().hex[:16]), so it isfresh per call but frozen across retries of that call.
The fallback-host loop is the only path that resends. Ruled out: httpx's default transport
is
retries=0; httpcore's pool only re-queues onConnectionNotAvailable, i.e. when therequest was never written; and
reauth_if_expiredreturns early forskip_auth, whichtoken requests pass.
For the test configuration
get_hosts()returns 3 hosts (5 endpoint fallbacks truncatedto
Defaults.http_max_retry_count = 3), so up to 2 fallback retries happen.Why the server rejects it
If the first POST reaches the server and its nonce is consumed, but the response is lost —
for example an HTTP/2 GOAWAY terminating the connection, which is live in the same CI runs
— the retry is a genuine replay.
test_request_token_with_duplicate_noncealready assertsthe server rejects a repeated (key, timestamp, nonce).
Reproduction
With a fake endpoint that consumes a nonce when it processes a request, returns
40105forany nonce it has already consumed, and drops the response to the first attempt:
40105 401 Nonce value replayedSuggested direction
Two options, both changing
make_request's contract:auth_callback/auth_urlpaths are not re-invoked, only the key-signed path.40105at the auth layer with a freshly generated token request. Smaller, leaveshttp.pyand RSC15 untouched, slightly wasteful if the first submission did succeed.More generally: any non-idempotent request is resent onto a fallback host after a lost
response. Token requests are just the case where the server detects and rejects it.
🤖 Generated with Claude Code
┆Issue is synchronized with this Jira Task by Unito