72
72
DEFAULT_TIMEOUT ,
73
73
DEFAULT_MAX_RETRIES ,
74
74
RAW_RESPONSE_HEADER ,
75
+ STREAMED_RAW_RESPONSE_HEADER ,
75
76
)
76
77
from ._streaming import Stream , AsyncStream
77
78
from ._exceptions import (
@@ -363,14 +364,21 @@ def _make_status_error_from_response(
363
364
self ,
364
365
response : httpx .Response ,
365
366
) -> APIStatusError :
366
- err_text = response .text .strip ()
367
- body = err_text
367
+ if response .is_closed and not response .is_stream_consumed :
368
+ # We can't read the response body as it has been closed
369
+ # before it was read. This can happen if an event hook
370
+ # raises a status error.
371
+ body = None
372
+ err_msg = f"Error code: { response .status_code } "
373
+ else :
374
+ err_text = response .text .strip ()
375
+ body = err_text
368
376
369
- try :
370
- body = json .loads (err_text )
371
- err_msg = f"Error code: { response .status_code } - { body } "
372
- except Exception :
373
- err_msg = err_text or f"Error code: { response .status_code } "
377
+ try :
378
+ body = json .loads (err_text )
379
+ err_msg = f"Error code: { response .status_code } - { body } "
380
+ except Exception :
381
+ err_msg = err_text or f"Error code: { response .status_code } "
374
382
375
383
return self ._make_status_error (err_msg , body = body , response = response )
376
384
@@ -534,6 +542,12 @@ def _process_response_data(
534
542
except pydantic .ValidationError as err :
535
543
raise APIResponseValidationError (response = response , body = data ) from err
536
544
545
+ def _should_stream_response_body (self , * , request : httpx .Request ) -> bool :
546
+ if request .headers .get (STREAMED_RAW_RESPONSE_HEADER ) == "true" :
547
+ return True
548
+
549
+ return False
550
+
537
551
@property
538
552
def qs (self ) -> Querystring :
539
553
return Querystring ()
@@ -606,7 +620,7 @@ def _calculate_retry_timeout(
606
620
if response_headers is not None :
607
621
retry_header = response_headers .get ("retry-after" )
608
622
try :
609
- retry_after = int (retry_header )
623
+ retry_after = float (retry_header )
610
624
except Exception :
611
625
retry_date_tuple = email .utils .parsedate_tz (retry_header )
612
626
if retry_date_tuple is None :
@@ -862,14 +876,21 @@ def _request(
862
876
request = self ._build_request (options )
863
877
self ._prepare_request (request )
864
878
879
+ response = None
880
+
865
881
try :
866
- response = self ._client .send (request , auth = self .custom_auth , stream = stream )
882
+ response = self ._client .send (
883
+ request ,
884
+ auth = self .custom_auth ,
885
+ stream = stream or self ._should_stream_response_body (request = request ),
886
+ )
867
887
log .debug (
868
888
'HTTP Request: %s %s "%i %s"' , request .method , request .url , response .status_code , response .reason_phrase
869
889
)
870
890
response .raise_for_status ()
871
891
except httpx .HTTPStatusError as err : # thrown on 4xx and 5xx status code
872
892
if retries > 0 and self ._should_retry (err .response ):
893
+ err .response .close ()
873
894
return self ._retry_request (
874
895
options ,
875
896
cast_to ,
@@ -881,27 +902,39 @@ def _request(
881
902
882
903
# If the response is streamed then we need to explicitly read the response
883
904
# to completion before attempting to access the response text.
884
- err .response .read ()
905
+ if not err .response .is_closed :
906
+ err .response .read ()
907
+
885
908
raise self ._make_status_error_from_response (err .response ) from None
886
909
except httpx .TimeoutException as err :
910
+ if response is not None :
911
+ response .close ()
912
+
887
913
if retries > 0 :
888
914
return self ._retry_request (
889
915
options ,
890
916
cast_to ,
891
917
retries ,
892
918
stream = stream ,
893
919
stream_cls = stream_cls ,
920
+ response_headers = response .headers if response is not None else None ,
894
921
)
922
+
895
923
raise APITimeoutError (request = request ) from err
896
924
except Exception as err :
925
+ if response is not None :
926
+ response .close ()
927
+
897
928
if retries > 0 :
898
929
return self ._retry_request (
899
930
options ,
900
931
cast_to ,
901
932
retries ,
902
933
stream = stream ,
903
934
stream_cls = stream_cls ,
935
+ response_headers = response .headers if response is not None else None ,
904
936
)
937
+
905
938
raise APIConnectionError (request = request ) from err
906
939
907
940
return self ._process_response (
@@ -917,7 +950,7 @@ def _retry_request(
917
950
options : FinalRequestOptions ,
918
951
cast_to : Type [ResponseT ],
919
952
remaining_retries : int ,
920
- response_headers : Optional [ httpx .Headers ] = None ,
953
+ response_headers : httpx .Headers | None ,
921
954
* ,
922
955
stream : bool ,
923
956
stream_cls : type [_StreamT ] | None ,
@@ -1303,14 +1336,21 @@ async def _request(
1303
1336
request = self ._build_request (options )
1304
1337
await self ._prepare_request (request )
1305
1338
1339
+ response = None
1340
+
1306
1341
try :
1307
- response = await self ._client .send (request , auth = self .custom_auth , stream = stream )
1342
+ response = await self ._client .send (
1343
+ request ,
1344
+ auth = self .custom_auth ,
1345
+ stream = stream or self ._should_stream_response_body (request = request ),
1346
+ )
1308
1347
log .debug (
1309
1348
'HTTP Request: %s %s "%i %s"' , request .method , request .url , response .status_code , response .reason_phrase
1310
1349
)
1311
1350
response .raise_for_status ()
1312
1351
except httpx .HTTPStatusError as err : # thrown on 4xx and 5xx status code
1313
1352
if retries > 0 and self ._should_retry (err .response ):
1353
+ await err .response .aclose ()
1314
1354
return await self ._retry_request (
1315
1355
options ,
1316
1356
cast_to ,
@@ -1322,19 +1362,39 @@ async def _request(
1322
1362
1323
1363
# If the response is streamed then we need to explicitly read the response
1324
1364
# to completion before attempting to access the response text.
1325
- await err .response .aread ()
1365
+ if not err .response .is_closed :
1366
+ await err .response .aread ()
1367
+
1326
1368
raise self ._make_status_error_from_response (err .response ) from None
1327
- except httpx .ConnectTimeout as err :
1328
- if retries > 0 :
1329
- return await self ._retry_request (options , cast_to , retries , stream = stream , stream_cls = stream_cls )
1330
- raise APITimeoutError (request = request ) from err
1331
1369
except httpx .TimeoutException as err :
1370
+ if response is not None :
1371
+ await response .aclose ()
1372
+
1332
1373
if retries > 0 :
1333
- return await self ._retry_request (options , cast_to , retries , stream = stream , stream_cls = stream_cls )
1374
+ return await self ._retry_request (
1375
+ options ,
1376
+ cast_to ,
1377
+ retries ,
1378
+ stream = stream ,
1379
+ stream_cls = stream_cls ,
1380
+ response_headers = response .headers if response is not None else None ,
1381
+ )
1382
+
1334
1383
raise APITimeoutError (request = request ) from err
1335
1384
except Exception as err :
1385
+ if response is not None :
1386
+ await response .aclose ()
1387
+
1336
1388
if retries > 0 :
1337
- return await self ._retry_request (options , cast_to , retries , stream = stream , stream_cls = stream_cls )
1389
+ return await self ._retry_request (
1390
+ options ,
1391
+ cast_to ,
1392
+ retries ,
1393
+ stream = stream ,
1394
+ stream_cls = stream_cls ,
1395
+ response_headers = response .headers if response is not None else None ,
1396
+ )
1397
+
1338
1398
raise APIConnectionError (request = request ) from err
1339
1399
1340
1400
return self ._process_response (
@@ -1350,7 +1410,7 @@ async def _retry_request(
1350
1410
options : FinalRequestOptions ,
1351
1411
cast_to : Type [ResponseT ],
1352
1412
remaining_retries : int ,
1353
- response_headers : Optional [ httpx .Headers ] = None ,
1413
+ response_headers : httpx .Headers | None ,
1354
1414
* ,
1355
1415
stream : bool ,
1356
1416
stream_cls : type [_AsyncStreamT ] | None ,
0 commit comments