Skip to content

Commit b90cd33

Browse files
feat(client): handle retry-after header with a date format (#113)
1 parent bd07c7f commit b90cd33

File tree

2 files changed

+72
-2
lines changed

2 files changed

+72
-2
lines changed

src/finch/_base_client.py

+16-2
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,10 @@
33
import json
44
import time
55
import uuid
6+
import email
67
import inspect
78
import platform
9+
import email.utils
810
from types import TracebackType
911
from random import random
1012
from typing import (
@@ -616,10 +618,22 @@ def _calculate_retry_timeout(
616618
try:
617619
# About the Retry-After header: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Retry-After
618620
#
619-
# TODO: we may want to handle the case where the header is using the http-date syntax: "Retry-After:
620621
# <http-date>". See https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Retry-After#syntax for
621622
# details.
622-
retry_after = -1 if response_headers is None else int(response_headers.get("retry-after"))
623+
if response_headers is not None:
624+
retry_header = response_headers.get("retry-after")
625+
try:
626+
retry_after = int(retry_header)
627+
except Exception:
628+
retry_date_tuple = email.utils.parsedate_tz(retry_header)
629+
if retry_date_tuple is None:
630+
retry_after = -1
631+
else:
632+
retry_date = email.utils.mktime_tz(retry_date_tuple)
633+
retry_after = int(retry_date - time.time())
634+
else:
635+
retry_after = -1
636+
623637
except Exception:
624638
retry_after = -1
625639

tests/test_client.py

+56
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import asyncio
88
import inspect
99
from typing import Any, Dict, Union, cast
10+
from unittest import mock
1011

1112
import httpx
1213
import pytest
@@ -420,6 +421,33 @@ class Model(BaseModel):
420421
response = client.get("/foo", cast_to=Model)
421422
assert isinstance(response, str) # type: ignore[unreachable]
422423

424+
@pytest.mark.parametrize(
425+
"remaining_retries,retry_after,timeout",
426+
[
427+
[3, "20", 20],
428+
[3, "0", 2],
429+
[3, "-10", 2],
430+
[3, "60", 60],
431+
[3, "61", 2],
432+
[3, "Fri, 29 Sep 2023 16:26:57 GMT", 20],
433+
[3, "Fri, 29 Sep 2023 16:26:37 GMT", 2],
434+
[3, "Fri, 29 Sep 2023 16:26:27 GMT", 2],
435+
[3, "Fri, 29 Sep 2023 16:27:37 GMT", 60],
436+
[3, "Fri, 29 Sep 2023 16:27:38 GMT", 2],
437+
[3, "99999999999999999999999999999999999", 2],
438+
[3, "Zun, 29 Sep 2023 16:26:27 GMT", 2],
439+
[3, "", 2],
440+
],
441+
)
442+
@mock.patch("time.time", mock.MagicMock(return_value=1696004797))
443+
def test_parse_retry_after_header(self, remaining_retries: int, retry_after: str, timeout: float) -> None:
444+
client = Finch(base_url=base_url, access_token=access_token, _strict_response_validation=True)
445+
446+
headers = httpx.Headers({"retry-after": retry_after})
447+
options = FinalRequestOptions(method="get", url="/foo", max_retries=2)
448+
calculated = client._calculate_retry_timeout(remaining_retries, options, headers)
449+
assert calculated == pytest.approx(timeout, 0.6) # pyright: ignore[reportUnknownMemberType]
450+
423451

424452
class TestAsyncFinch:
425453
client = AsyncFinch(base_url=base_url, access_token=access_token, _strict_response_validation=True)
@@ -815,3 +843,31 @@ class Model(BaseModel):
815843

816844
response = await client.get("/foo", cast_to=Model)
817845
assert isinstance(response, str) # type: ignore[unreachable]
846+
847+
@pytest.mark.parametrize(
848+
"remaining_retries,retry_after,timeout",
849+
[
850+
[3, "20", 20],
851+
[3, "0", 2],
852+
[3, "-10", 2],
853+
[3, "60", 60],
854+
[3, "61", 2],
855+
[3, "Fri, 29 Sep 2023 16:26:57 GMT", 20],
856+
[3, "Fri, 29 Sep 2023 16:26:37 GMT", 2],
857+
[3, "Fri, 29 Sep 2023 16:26:27 GMT", 2],
858+
[3, "Fri, 29 Sep 2023 16:27:37 GMT", 60],
859+
[3, "Fri, 29 Sep 2023 16:27:38 GMT", 2],
860+
[3, "99999999999999999999999999999999999", 2],
861+
[3, "Zun, 29 Sep 2023 16:26:27 GMT", 2],
862+
[3, "", 2],
863+
],
864+
)
865+
@mock.patch("time.time", mock.MagicMock(return_value=1696004797))
866+
@pytest.mark.asyncio
867+
async def test_parse_retry_after_header(self, remaining_retries: int, retry_after: str, timeout: float) -> None:
868+
client = AsyncFinch(base_url=base_url, access_token=access_token, _strict_response_validation=True)
869+
870+
headers = httpx.Headers({"retry-after": retry_after})
871+
options = FinalRequestOptions(method="get", url="/foo", max_retries=2)
872+
calculated = client._calculate_retry_timeout(remaining_retries, options, headers)
873+
assert calculated == pytest.approx(timeout, 0.6) # pyright: ignore[reportUnknownMemberType]

0 commit comments

Comments
 (0)