Skip to content

Commit

Permalink
feat(client): handle retry-after header with a date format (#113)
Browse files Browse the repository at this point in the history
  • Loading branch information
stainless-bot committed Oct 5, 2023
1 parent c91e4a7 commit 6fe5576
Show file tree
Hide file tree
Showing 2 changed files with 72 additions and 2 deletions.
18 changes: 16 additions & 2 deletions src/finch/_base_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,10 @@
import json
import time
import uuid
import email
import inspect
import platform
import email.utils
from types import TracebackType
from random import random
from typing import (
Expand Down Expand Up @@ -616,10 +618,22 @@ def _calculate_retry_timeout(
try:
# About the Retry-After header: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Retry-After
#
# TODO: we may want to handle the case where the header is using the http-date syntax: "Retry-After:
# <http-date>". See https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Retry-After#syntax for
# details.
retry_after = -1 if response_headers is None else int(response_headers.get("retry-after"))
if response_headers is not None:
retry_header = response_headers.get("retry-after")
try:
retry_after = int(retry_header)
except Exception:
retry_date_tuple = email.utils.parsedate_tz(retry_header)
if retry_date_tuple is None:
retry_after = -1
else:
retry_date = email.utils.mktime_tz(retry_date_tuple)
retry_after = int(retry_date - time.time())
else:
retry_after = -1

except Exception:
retry_after = -1

Expand Down
56 changes: 56 additions & 0 deletions tests/test_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import asyncio
import inspect
from typing import Any, Dict, Union, cast
from unittest import mock

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

@pytest.mark.parametrize(
"remaining_retries,retry_after,timeout",
[
[3, "20", 20],
[3, "0", 2],
[3, "-10", 2],
[3, "60", 60],
[3, "61", 2],
[3, "Fri, 29 Sep 2023 16:26:57 GMT", 20],
[3, "Fri, 29 Sep 2023 16:26:37 GMT", 2],
[3, "Fri, 29 Sep 2023 16:26:27 GMT", 2],
[3, "Fri, 29 Sep 2023 16:27:37 GMT", 60],
[3, "Fri, 29 Sep 2023 16:27:38 GMT", 2],
[3, "99999999999999999999999999999999999", 2],
[3, "Zun, 29 Sep 2023 16:26:27 GMT", 2],
[3, "", 2],
],
)
@mock.patch("time.time", mock.MagicMock(return_value=1696004797))
def test_parse_retry_after_header(self, remaining_retries: int, retry_after: str, timeout: float) -> None:
client = Finch(base_url=base_url, access_token=access_token, _strict_response_validation=True)

headers = httpx.Headers({"retry-after": retry_after})
options = FinalRequestOptions(method="get", url="/foo", max_retries=2)
calculated = client._calculate_retry_timeout(remaining_retries, options, headers)
assert calculated == pytest.approx(timeout, 0.6) # pyright: ignore[reportUnknownMemberType]


class TestAsyncFinch:
client = AsyncFinch(base_url=base_url, access_token=access_token, _strict_response_validation=True)
Expand Down Expand Up @@ -815,3 +843,31 @@ class Model(BaseModel):

response = await client.get("/foo", cast_to=Model)
assert isinstance(response, str) # type: ignore[unreachable]

@pytest.mark.parametrize(
"remaining_retries,retry_after,timeout",
[
[3, "20", 20],
[3, "0", 2],
[3, "-10", 2],
[3, "60", 60],
[3, "61", 2],
[3, "Fri, 29 Sep 2023 16:26:57 GMT", 20],
[3, "Fri, 29 Sep 2023 16:26:37 GMT", 2],
[3, "Fri, 29 Sep 2023 16:26:27 GMT", 2],
[3, "Fri, 29 Sep 2023 16:27:37 GMT", 60],
[3, "Fri, 29 Sep 2023 16:27:38 GMT", 2],
[3, "99999999999999999999999999999999999", 2],
[3, "Zun, 29 Sep 2023 16:26:27 GMT", 2],
[3, "", 2],
],
)
@mock.patch("time.time", mock.MagicMock(return_value=1696004797))
@pytest.mark.asyncio
async def test_parse_retry_after_header(self, remaining_retries: int, retry_after: str, timeout: float) -> None:
client = AsyncFinch(base_url=base_url, access_token=access_token, _strict_response_validation=True)

headers = httpx.Headers({"retry-after": retry_after})
options = FinalRequestOptions(method="get", url="/foo", max_retries=2)
calculated = client._calculate_retry_timeout(remaining_retries, options, headers)
assert calculated == pytest.approx(timeout, 0.6) # pyright: ignore[reportUnknownMemberType]

0 comments on commit 6fe5576

Please sign in to comment.