Skip to content

Add RateLimitError for HTTP 429 responses from Google recognizer - #904

Open
harshitayadavv wants to merge 3 commits into
Uberi:masterfrom
harshitayadavv:add-rate-limit-error
Open

Add RateLimitError for HTTP 429 responses from Google recognizer#904
harshitayadavv wants to merge 3 commits into
Uberi:masterfrom
harshitayadavv:add-rate-limit-error

Conversation

@harshitayadavv

Copy link
Copy Markdown

Problem

obtain_transcription() in speech_recognition/recognizers/google.py
catches HTTPError and wraps every case -- including HTTP 429 (rate
limited) -- into a generic RequestError, using only e.reason (a
string like "Too Many Requests") and discarding e.code entirely.
This makes it impossible to:

  • catch rate limiting separately from other request failures (e.g. bad
    key, network issues)
  • implement proper backoff, since the Retry-After header is never
    surfaced

Reproduction

import speech_recognition as sr
from unittest.mock import patch
from urllib.error import HTTPError

r = sr.Recognizer()
with patch("speech_recognition.recognizers.google.urlopen") as m:
    m.side_effect = HTTPError(
        url="", code=429, msg="Too Many Requests",
        hdrs={"Retry-After": "30"}, fp=None,
    )
    r.recognize_google(audio_data)
# raises RequestError -- can't distinguish from any other failure,
# Retry-After is silently discarded

Fix

Adds RateLimitError(RequestError) in speech_recognition/exceptions.py
with a retry_after: float | None attribute, parsed from the
Retry-After header when the service returns HTTP 429. Since it
subclasses RequestError, this is backward compatible -- existing code
catching RequestError continues to work unchanged, while callers who
want to specifically detect and back off on rate limiting can now catch
RateLimitError.

Includes a test in tests/recognizers/test_google.py covering the 429
case and confirming retry_after parses correctly.

Ran the full test suite, flake8, and mypy locally -- all clean
except pre-existing failures from optional recognizer backends not
installed in this environment (unrelated to this change).

obtain_transcription() previously wrapped every HTTPError -- including
rate limiting (429) -- into a generic RequestError using only e.reason,
discarding the actual status code. This made it impossible to catch
rate limiting separately from other request failures, or to implement
backoff using the Retry-After header.

Adds a RateLimitError(RequestError) subclass with a retry_after
attribute parsed from the Retry-After header when present. 429
responses now raise RateLimitError specifically; all other HTTPErrors
continue to raise RequestError as before, so this is backward
compatible for anyone already catching RequestError.

Includes a test covering the 429 case and confirming retry_after is
parsed correctly. Ran full test suite, flake8, and mypy locally --
all clean except pre-existing, unrelated failures from optional
recognizer backends not installed in this environment (cohere, vosk,
groq, openai, faster_whisper, soundfile, pocketsphinx,
google.cloud.speech).
@ftnext ftnext self-assigned this Jul 29, 2026
@ftnext

ftnext commented Jul 29, 2026

Copy link
Copy Markdown
Collaborator

@harshitayadavv Thanks for the proposal. This looks like a useful improvement.
Before merging, could you share an actual 429 response or Google documentation confirming that this endpoint returns a numeric Retry-After header?

@ftnext ftnext assigned harshitayadavv and unassigned ftnext Jul 29, 2026
Per @ftnext's review: Retry-After can be a non-negative integer number
of seconds OR an HTTP-date, per RFC 9110 10.2.3. The previous
implementation only handled the numeric form via float(), silently
discarding a date-formatted header instead of parsing it.

Note: I could not find official Google documentation for this specific
endpoint's Retry-After format, since it's an undocumented legacy API
(the same one Chromium uses internally, accessed via a hardcoded key).
Rather than assert an unverifiable claim about the exact format Google
returns, this makes the parsing correct for both valid forms per the
HTTP spec, so the behavior is correct regardless of which one is
actually used.
@harshitayadavv

Copy link
Copy Markdown
Author

Good catch, and you're right to ask, I can't point to official documentation for this specific endpoint's Retry-After format, since it's an undocumented legacy API (the same one Chromium uses internally). Rather than assume, I've updated the parsing to correctly handle both valid forms defined by RFC 9110 §10.2.3 (seconds or an HTTP-date), so it's correct regardless of which one this endpoint actually sends. Added a test covering the HTTP-date case too.

@ftnext

ftnext commented Jul 30, 2026

Copy link
Copy Markdown
Collaborator

@harshitayadavv Thanks for the update and for making the parser RFC-compliant. However, my main question is whether this Google endpoint actually returns a Retry-After header at all, rather than which format it uses. Without documentation or an observed response confirming that, the parser and the public retry_after attribute seem speculative.

Could you provide evidence that an actual 429 response includes this header? Otherwise, I would prefer to keep this change scoped to raising RateLimitError for HTTP 429 and omit Retry-After handling until it is documented or observed.

Per @ftnext's follow-up review: drop the retry_after attribute and
Retry-After parsing entirely, since we have no documentation or
observed evidence that this endpoint actually sends that header at
all. Keeping the change scoped to just raising RateLimitError for
HTTP 429, which is directly observable and doesn't rely on any
unverified assumption about response headers.
@harshitayadavv

Copy link
Copy Markdown
Author

That's a fair point, I don't have evidence this endpoint sends Retry-After at all, so asserting a retry_after attribute on RateLimitError would be speculative. I've removed it entirely and scoped this back down to just raising RateLimitError for HTTP 429, which is the part I can actually observe and verify.

@ftnext

ftnext commented Aug 1, 2026

Copy link
Copy Markdown
Collaborator

@harshitayadavv Thanks for simplifying the change. However, now that Retry-After handling has been removed, this PR only distinguishes HTTP 429 from other request failures through a new public exception type.
Even if this endpoint returns 429, I am not yet convinced that callers can meaningfully handle it differently without retry guidance from the service. Could you provide a concrete use case where catching RateLimitError enables behavior that would not be appropriate with the existing RequestError?
Without a demonstrated need for that distinction, I do not currently see a strong reason to add and maintain this public API, so I would prefer not to merge the PR.

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.

2 participants