feat(errors): expose Retry-After header on UsageLimitExceededError#166
Open
stihahi wants to merge 2 commits intotavily-ai:masterfrom
Open
feat(errors): expose Retry-After header on UsageLimitExceededError#166stihahi wants to merge 2 commits intotavily-ai:masterfrom
stihahi wants to merge 2 commits intotavily-ai:masterfrom
Conversation
The Tavily API returns a Retry-After header on 429 Too Many Requests responses, but the SDK currently discards it. Callers implementing their own client-side throttling fall back to guessing a backoff instead of honoring the server's recommendation. This change: - Adds a retry_after: Optional[float] attribute to UsageLimitExceededError, defaulting to None for backward compatibility. - Adds parse_retry_after() in tavily.errors which accepts either form defined by RFC 7231 §7.1.3 (non-negative seconds or HTTP-date) and returns None when the header is absent or unparseable. - Populates retry_after at every 429 raise site in TavilyClient and AsyncTavilyClient (search, extract, crawl, map, research, research streaming). - Adds tests covering: integer seconds, HTTP-date, missing header, malformed value, sync + async paths, and the error constructor itself. No breaking changes: existing `except UsageLimitExceededError as e: ...` code keeps working; callers that want to honor the header now read e.retry_after.
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 46de255. Configure here.
Addresses upstream review feedback:
- Reject fractional/non-finite seconds. RFC 7231 §7.1.3 defines the
numeric form as a non-negative integer. Previous `float(raw)` accepted
"7.5", "nan", "inf", "-10", which could land as retry_after on the
exception and crash callers (time.sleep(nan) raises ValueError).
Match urllib3.util.Retry.parse_retry_after semantics: parse int first,
then HTTP-date, clamp negatives/past dates to 0.
- Make header lookup explicitly case-insensitive by iterating items(),
so the helper is correct for any Mapping (not only the case-insensitive
containers from requests/httpx).
- Rename parse_retry_after -> _parse_retry_after to signal internal-only
API; not re-exported from tavily/__init__.py.
- Add Mapping[str, str] type annotation on the headers parameter for
consistency with the rest of errors.py.
Test additions:
- fractional seconds rejected
- negative integer seconds clamp to 0
- NaN/inf rejected
- case-insensitive header name ("retry-after", "RETRY-AFTER")
- past HTTP-date clamps to 0
- empty/whitespace-only header values return None
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.

Summary
The Tavily REST API returns a
Retry-Afterheader on429 Too Many Requestsresponses (docs),but the Python SDK currently discards it and raises
UsageLimitExceededError(detail)with no way for callers to recover theserver's recommended wait. Applications implementing client-side
throttling (e.g. production workloads hitting the 1000 RPM tier) have to
fall back to a fixed exponential backoff and guess at the real cool-down.
This PR exposes the header value on the exception so callers can honor
the server's recommendation.
Changes
tavily/errors.pyAdd
UsageLimitExceededError.retry_after: Optional[float], defaultingto
Nonefor backward compatibility.Add internal
_parse_retry_after(headers)helper. Semantics followurllib3.util.Retry.parse_retry_after— parseRFC 7231 §7.1.3:
"120"(clamped to ≥ 0)"Wed, 21 Oct 2015 07:28:00 GMT"(converted toseconds from now, past dates clamp to 0)
Returns
Nonewhen the header is absent, empty, fractional,NaN/inf,or otherwise unparseable.
Case-insensitive header name lookup is done explicitly, so callers
passing a plain
dict(not onlyrequests/httpxheader containers)work correctly.
tavily/tavily.py,tavily/async_tavily.py429raise site (14 total: sync + async × search, extract,crawl, map, research request, research streaming), pass
retry_after=parse_retry_after(response.headers).tests/test_retry_after.py(new)Covers:
Retry-Afteris exposed on the exceptionRetry-Afteris parsed and exposedRetry-Afteryieldsretry_after=Noneretry_after=Noneretry_afterdefaults toNoneand accepts explicit valueNaN/infrejected, case-insensitive header name, past HTTP-date clamps to 0,
empty/whitespace-only value returns
NoneAll existing tests still pass (
88 passedincluding the 14 new cases).Backward compatibility
Non-breaking.
UsageLimitExceededError(message)still works —retry_afteris adefault-
Nonekwarg.except UsageLimitExceededError as e: str(e)areunaffected.
except UsageLimitExceededError as e: sleep(e.retry_after or fallback).Example
Notes for reviewers
retry_after=parse_retry_after(response.headers)— a follow-up couldextract the full error-response-to-exception mapping into a shared
helper, but that refactor is independent of this fix and intentionally
deferred.
parse_retry_afteraccepts any mapping (works for bothrequestsCaseInsensitiveDictandhttpxHeaders).email.utils.parsedate_to_datetime(stdlib).Naive datetimes are assumed UTC.