Skip to content

Retry-After is never read on 429 retries: dict(httpx.Headers) lowercases the key #17

Description

@shachar-fastsimon

Summary

In admin_graphql_request, the Retry-After header is never read on a 429 response. The lookup always falls through to its "1" default, so every rate-limit retry sleeps exactly one second regardless of what the server asked for.

Affects both the sync and async paths.

Version: shopifyapp 1.0.1 (sdist from PyPI), httpx 0.28.1.

Cause

shopify_app/graphql/admin_graphql.py:111 (and :708 on the async path) normalizes the response headers with:

response_headers = dict(response.headers)

dict() on an httpx.Headers instance produces lowercased keys. The 429 handlers then look the header up with its canonical casing, at shopify_app/graphql/admin_graphql.py:389 (sync) and :445 (async):

retry_after = response_headers.get("Retry-After", "1")

That key is never present, so retry_after is always the literal string "1".

Reproduction

import httpx

h = httpx.Headers({"Retry-After": "2.0", "Content-Type": "application/json"})
d = dict(h)

print(list(d))                     # ['retry-after', 'content-type']
print(d.get("Retry-After", "1"))   # '1'   <- expected '2.0'

Impact

The client ignores server-provided backoff on rate limiting and retries on a fixed one-second interval instead. With the default max_retries=2 that is about two seconds of total backoff, typically well short of what a rate-limited endpoint asks for.

Suggested fix

httpx.Headers is already case-insensitive, so reading from the response object directly avoids the problem:

retry_after = response.headers.get("Retry-After", "1")

Keeping the dict and looking up the lowercase key works too.

One caveat: fixing this lookup on its own exposes a second defect on the same value, where int(retry_after) raises ValueError on anything that is not a bare integer. Filed separately as #18, which also notes that the two are best addressed together.

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

    devtools-gardenerPost the issue or PR to Slack for the gardener

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions