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

Open Beginner friendly
#17 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Assessment

Difficulty
2/5
Estimated time
1-3 hours
Newbie friendliness
78/100
Issue type
Bug
Clarity
Clearly specified
Activity status
Active
Tech stack
python
Domain
api, backend

Research direction

Start with shopify_app/graphql/admin_graphql.py at lines 111 and 708, then inspect the sync and async 429 handlers at lines 389 and 445. Reproduce the header-key behavior with the provided httpx snippet, and confirm both retry paths use the server-provided Retry-After value rather than the default.

Written by the indexing model from the issue text.

Description

devtools-gardener

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.

Dominant language
Python
Stars
17
Forks
1
Avg merge
3m
Merged PRs (30d)
2

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

More from Shopify/shopify-app-python

All issues in Shopify/shopify-app-python

Similar issues

More Python issues

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.