Skip to content

A retried token request re-sends the same nonce, so the server rejects it with 40105 #710

Description

@owenpearson

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:

  1. 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.
  2. 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

Activity

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions