@@ -400,14 +400,7 @@ def _make_status_error(
400400 ) -> _exceptions .APIStatusError :
401401 raise NotImplementedError ()
402402
403- def _remaining_retries (
404- self ,
405- remaining_retries : Optional [int ],
406- options : FinalRequestOptions ,
407- ) -> int :
408- return remaining_retries if remaining_retries is not None else options .get_max_retries (self .max_retries )
409-
410- def _build_headers (self , options : FinalRequestOptions ) -> httpx .Headers :
403+ def _build_headers (self , options : FinalRequestOptions , * , retries_taken : int = 0 ) -> httpx .Headers :
411404 custom_headers = options .headers or {}
412405 headers_dict = _merge_mappings (self .default_headers , custom_headers )
413406 self ._validate_headers (headers_dict , custom_headers )
@@ -419,6 +412,11 @@ def _build_headers(self, options: FinalRequestOptions) -> httpx.Headers:
419412 if idempotency_header and options .method .lower () != "get" and idempotency_header not in headers :
420413 headers [idempotency_header ] = options .idempotency_key or self ._idempotency_key ()
421414
415+ # Don't set the retry count header if it was already set or removed by the caller. We check
416+ # `custom_headers`, which can contain `Omit()`, instead of `headers` to account for the removal case.
417+ if "x-stainless-retry-count" not in (header .lower () for header in custom_headers ):
418+ headers ["x-stainless-retry-count" ] = str (retries_taken )
419+
422420 return headers
423421
424422 def _prepare_url (self , url : str ) -> URL :
@@ -440,6 +438,8 @@ def _make_sse_decoder(self) -> SSEDecoder | SSEBytesDecoder:
440438 def _build_request (
441439 self ,
442440 options : FinalRequestOptions ,
441+ * ,
442+ retries_taken : int = 0 ,
443443 ) -> httpx .Request :
444444 if log .isEnabledFor (logging .DEBUG ):
445445 log .debug ("Request options: %s" , model_dump (options , exclude_unset = True ))
@@ -455,7 +455,7 @@ def _build_request(
455455 else :
456456 raise RuntimeError (f"Unexpected JSON data type, { type (json_data )} , cannot merge with `extra_body`" )
457457
458- headers = self ._build_headers (options )
458+ headers = self ._build_headers (options , retries_taken = retries_taken )
459459 params = _merge_mappings (self .default_query , options .params )
460460 content_type = headers .get ("Content-Type" )
461461 files = options .files
@@ -489,12 +489,17 @@ def _build_request(
489489 if not files :
490490 files = cast (HttpxRequestFiles , ForceMultipartDict ())
491491
492+ prepared_url = self ._prepare_url (options .url )
493+ if "_" in prepared_url .host :
494+ # work around https://github.com/encode/httpx/discussions/2880
495+ kwargs ["extensions" ] = {"sni_hostname" : prepared_url .host .replace ("_" , "-" )}
496+
492497 # TODO: report this error to httpx
493498 return self ._client .build_request ( # pyright: ignore[reportUnknownMemberType]
494499 headers = headers ,
495500 timeout = self .timeout if isinstance (options .timeout , NotGiven ) else options .timeout ,
496501 method = options .method ,
497- url = self . _prepare_url ( options . url ) ,
502+ url = prepared_url ,
498503 # the `Query` type that we use is incompatible with qs'
499504 # `Params` type as it needs to be typed as `Mapping[str, object]`
500505 # so that passing a `TypedDict` doesn't cause an error.
@@ -933,20 +938,25 @@ def request(
933938 stream : bool = False ,
934939 stream_cls : type [_StreamT ] | None = None ,
935940 ) -> ResponseT | _StreamT :
941+ if remaining_retries is not None :
942+ retries_taken = options .get_max_retries (self .max_retries ) - remaining_retries
943+ else :
944+ retries_taken = 0
945+
936946 return self ._request (
937947 cast_to = cast_to ,
938948 options = options ,
939949 stream = stream ,
940950 stream_cls = stream_cls ,
941- remaining_retries = remaining_retries ,
951+ retries_taken = retries_taken ,
942952 )
943953
944954 def _request (
945955 self ,
946956 * ,
947957 cast_to : Type [ResponseT ],
948958 options : FinalRequestOptions ,
949- remaining_retries : int | None ,
959+ retries_taken : int ,
950960 stream : bool ,
951961 stream_cls : type [_StreamT ] | None ,
952962 ) -> ResponseT | _StreamT :
@@ -958,8 +968,8 @@ def _request(
958968 cast_to = self ._maybe_override_cast_to (cast_to , options )
959969 options = self ._prepare_options (options )
960970
961- retries = self . _remaining_retries ( remaining_retries , options )
962- request = self ._build_request (options )
971+ remaining_retries = options . get_max_retries ( self . max_retries ) - retries_taken
972+ request = self ._build_request (options , retries_taken = retries_taken )
963973 self ._prepare_request (request )
964974
965975 kwargs : HttpxSendArgs = {}
@@ -977,11 +987,11 @@ def _request(
977987 except httpx .TimeoutException as err :
978988 log .debug ("Encountered httpx.TimeoutException" , exc_info = True )
979989
980- if retries > 0 :
990+ if remaining_retries > 0 :
981991 return self ._retry_request (
982992 input_options ,
983993 cast_to ,
984- retries ,
994+ retries_taken = retries_taken ,
985995 stream = stream ,
986996 stream_cls = stream_cls ,
987997 response_headers = None ,
@@ -992,11 +1002,11 @@ def _request(
9921002 except Exception as err :
9931003 log .debug ("Encountered Exception" , exc_info = True )
9941004
995- if retries > 0 :
1005+ if remaining_retries > 0 :
9961006 return self ._retry_request (
9971007 input_options ,
9981008 cast_to ,
999- retries ,
1009+ retries_taken = retries_taken ,
10001010 stream = stream ,
10011011 stream_cls = stream_cls ,
10021012 response_headers = None ,
@@ -1019,13 +1029,13 @@ def _request(
10191029 except httpx .HTTPStatusError as err : # thrown on 4xx and 5xx status code
10201030 log .debug ("Encountered httpx.HTTPStatusError" , exc_info = True )
10211031
1022- if retries > 0 and self ._should_retry (err .response ):
1032+ if remaining_retries > 0 and self ._should_retry (err .response ):
10231033 err .response .close ()
10241034 return self ._retry_request (
10251035 input_options ,
10261036 cast_to ,
1027- retries ,
1028- err .response .headers ,
1037+ retries_taken = retries_taken ,
1038+ response_headers = err .response .headers ,
10291039 stream = stream ,
10301040 stream_cls = stream_cls ,
10311041 )
@@ -1044,26 +1054,26 @@ def _request(
10441054 response = response ,
10451055 stream = stream ,
10461056 stream_cls = stream_cls ,
1047- retries_taken = options . get_max_retries ( self . max_retries ) - retries ,
1057+ retries_taken = retries_taken ,
10481058 )
10491059
10501060 def _retry_request (
10511061 self ,
10521062 options : FinalRequestOptions ,
10531063 cast_to : Type [ResponseT ],
1054- remaining_retries : int ,
1055- response_headers : httpx .Headers | None ,
10561064 * ,
1065+ retries_taken : int ,
1066+ response_headers : httpx .Headers | None ,
10571067 stream : bool ,
10581068 stream_cls : type [_StreamT ] | None ,
10591069 ) -> ResponseT | _StreamT :
1060- remaining = remaining_retries - 1
1061- if remaining == 1 :
1070+ remaining_retries = options . get_max_retries ( self . max_retries ) - retries_taken
1071+ if remaining_retries == 1 :
10621072 log .debug ("1 retry left" )
10631073 else :
1064- log .debug ("%i retries left" , remaining )
1074+ log .debug ("%i retries left" , remaining_retries )
10651075
1066- timeout = self ._calculate_retry_timeout (remaining , options , response_headers )
1076+ timeout = self ._calculate_retry_timeout (remaining_retries , options , response_headers )
10671077 log .info ("Retrying request to %s in %f seconds" , options .url , timeout )
10681078
10691079 # In a synchronous context we are blocking the entire thread. Up to the library user to run the client in a
@@ -1073,7 +1083,7 @@ def _retry_request(
10731083 return self ._request (
10741084 options = options ,
10751085 cast_to = cast_to ,
1076- remaining_retries = remaining ,
1086+ retries_taken = retries_taken + 1 ,
10771087 stream = stream ,
10781088 stream_cls = stream_cls ,
10791089 )
@@ -1491,12 +1501,17 @@ async def request(
14911501 stream_cls : type [_AsyncStreamT ] | None = None ,
14921502 remaining_retries : Optional [int ] = None ,
14931503 ) -> ResponseT | _AsyncStreamT :
1504+ if remaining_retries is not None :
1505+ retries_taken = options .get_max_retries (self .max_retries ) - remaining_retries
1506+ else :
1507+ retries_taken = 0
1508+
14941509 return await self ._request (
14951510 cast_to = cast_to ,
14961511 options = options ,
14971512 stream = stream ,
14981513 stream_cls = stream_cls ,
1499- remaining_retries = remaining_retries ,
1514+ retries_taken = retries_taken ,
15001515 )
15011516
15021517 async def _request (
@@ -1506,7 +1521,7 @@ async def _request(
15061521 * ,
15071522 stream : bool ,
15081523 stream_cls : type [_AsyncStreamT ] | None ,
1509- remaining_retries : int | None ,
1524+ retries_taken : int ,
15101525 ) -> ResponseT | _AsyncStreamT :
15111526 if self ._platform is None :
15121527 # `get_platform` can make blocking IO calls so we
@@ -1521,8 +1536,8 @@ async def _request(
15211536 cast_to = self ._maybe_override_cast_to (cast_to , options )
15221537 options = await self ._prepare_options (options )
15231538
1524- retries = self . _remaining_retries ( remaining_retries , options )
1525- request = self ._build_request (options )
1539+ remaining_retries = options . get_max_retries ( self . max_retries ) - retries_taken
1540+ request = self ._build_request (options , retries_taken = retries_taken )
15261541 await self ._prepare_request (request )
15271542
15281543 kwargs : HttpxSendArgs = {}
@@ -1538,11 +1553,11 @@ async def _request(
15381553 except httpx .TimeoutException as err :
15391554 log .debug ("Encountered httpx.TimeoutException" , exc_info = True )
15401555
1541- if retries > 0 :
1556+ if remaining_retries > 0 :
15421557 return await self ._retry_request (
15431558 input_options ,
15441559 cast_to ,
1545- retries ,
1560+ retries_taken = retries_taken ,
15461561 stream = stream ,
15471562 stream_cls = stream_cls ,
15481563 response_headers = None ,
@@ -1553,11 +1568,11 @@ async def _request(
15531568 except Exception as err :
15541569 log .debug ("Encountered Exception" , exc_info = True )
15551570
1556- if retries > 0 :
1571+ if retries_taken > 0 :
15571572 return await self ._retry_request (
15581573 input_options ,
15591574 cast_to ,
1560- retries ,
1575+ retries_taken = retries_taken ,
15611576 stream = stream ,
15621577 stream_cls = stream_cls ,
15631578 response_headers = None ,
@@ -1575,13 +1590,13 @@ async def _request(
15751590 except httpx .HTTPStatusError as err : # thrown on 4xx and 5xx status code
15761591 log .debug ("Encountered httpx.HTTPStatusError" , exc_info = True )
15771592
1578- if retries > 0 and self ._should_retry (err .response ):
1593+ if remaining_retries > 0 and self ._should_retry (err .response ):
15791594 await err .response .aclose ()
15801595 return await self ._retry_request (
15811596 input_options ,
15821597 cast_to ,
1583- retries ,
1584- err .response .headers ,
1598+ retries_taken = retries_taken ,
1599+ response_headers = err .response .headers ,
15851600 stream = stream ,
15861601 stream_cls = stream_cls ,
15871602 )
@@ -1600,34 +1615,34 @@ async def _request(
16001615 response = response ,
16011616 stream = stream ,
16021617 stream_cls = stream_cls ,
1603- retries_taken = options . get_max_retries ( self . max_retries ) - retries ,
1618+ retries_taken = retries_taken ,
16041619 )
16051620
16061621 async def _retry_request (
16071622 self ,
16081623 options : FinalRequestOptions ,
16091624 cast_to : Type [ResponseT ],
1610- remaining_retries : int ,
1611- response_headers : httpx .Headers | None ,
16121625 * ,
1626+ retries_taken : int ,
1627+ response_headers : httpx .Headers | None ,
16131628 stream : bool ,
16141629 stream_cls : type [_AsyncStreamT ] | None ,
16151630 ) -> ResponseT | _AsyncStreamT :
1616- remaining = remaining_retries - 1
1617- if remaining == 1 :
1631+ remaining_retries = options . get_max_retries ( self . max_retries ) - retries_taken
1632+ if remaining_retries == 1 :
16181633 log .debug ("1 retry left" )
16191634 else :
1620- log .debug ("%i retries left" , remaining )
1635+ log .debug ("%i retries left" , remaining_retries )
16211636
1622- timeout = self ._calculate_retry_timeout (remaining , options , response_headers )
1637+ timeout = self ._calculate_retry_timeout (remaining_retries , options , response_headers )
16231638 log .info ("Retrying request to %s in %f seconds" , options .url , timeout )
16241639
16251640 await anyio .sleep (timeout )
16261641
16271642 return await self ._request (
16281643 options = options ,
16291644 cast_to = cast_to ,
1630- remaining_retries = remaining ,
1645+ retries_taken = retries_taken + 1 ,
16311646 stream = stream ,
16321647 stream_cls = stream_cls ,
16331648 )
0 commit comments