@@ -215,6 +215,17 @@ async def text(self):
215215 return json .dumps (self .payload )
216216
217217
218+ class _UndecodableResponse :
219+ async def json (self , ** kwargs ):
220+ raise UnicodeDecodeError ("utf-8" , b"\xff " , 0 , 1 , "invalid start byte" )
221+
222+ async def text (self ):
223+ raise UnicodeDecodeError ("utf-8" , b"\xff " , 0 , 1 , "invalid start byte" )
224+
225+ async def read (self ):
226+ return b"\xff "
227+
228+
218229class _RecordingSession :
219230 def __init__ (self , payload ):
220231 self .calls = []
@@ -406,6 +417,13 @@ async def test_async_client_delete_template_get_bill_and_plain_text_fallback(sel
406417 self .assertEqual (response .status_code , 200 )
407418 self .assertEqual (response .headers ["X-Async-Route" ], "get_assembly_plain" )
408419
420+ async def test_async_request_falls_back_to_bytes_when_text_decode_fails (self ):
421+ client = AsyncTransloadit ("key" , "secret" , service = self .server .base_url )
422+
423+ data = await client .request ._read_response_data (_UndecodableResponse ())
424+
425+ self .assertEqual (data , b"\xff " )
426+
409427 async def test_async_assembly_create_raises_on_plain_text_error_response (self ):
410428 plain_response = Response (
411429 data = "plain assembly response" ,
@@ -670,6 +688,10 @@ def __init__(self, tus_url):
670688
671689 self .assertEqual (response .data ["ok" ], "ASSEMBLY_COMPLETED" )
672690 post_mock .assert_awaited_once ()
691+ self .assertEqual (
692+ post_mock .await_args .kwargs ["extra_data" ],
693+ {"tus_num_expected_upload_files" : 0 },
694+ )
673695 get_mock .assert_awaited_once_with (
674696 assembly_url = f"{ self .server .base_url } /assemblies/assembly-123"
675697 )
@@ -1040,6 +1062,55 @@ async def test_async_assembly_wait_retries_after_polling_rate_limit(self):
10401062 )
10411063 self .assertEqual (sleep_mock .await_args_list , [mock .call (0 ), mock .call (0 )])
10421064
1065+ async def test_async_assembly_wait_does_not_follow_poll_response_assembly_url (self ):
1066+ initial_url = f"{ self .server .base_url } /assemblies/assembly-123"
1067+
1068+ async with AsyncTransloadit ("key" , "secret" , service = self .server .base_url ) as client :
1069+ assembly = client .new_assembly ()
1070+
1071+ initial = Response (
1072+ data = {
1073+ "ok" : "ASSEMBLY_PROCESSING" ,
1074+ "info" : {"retryIn" : 0 },
1075+ "assembly_ssl_url" : initial_url ,
1076+ },
1077+ status_code = 200 ,
1078+ headers = {},
1079+ )
1080+ malicious_poll = Response (
1081+ data = {
1082+ "ok" : "ASSEMBLY_PROCESSING" ,
1083+ "error" : "ASSEMBLY_STATUS_FETCHING_RATE_LIMIT_REACHED" ,
1084+ "info" : {"retryIn" : 0 },
1085+ "assembly_ssl_url" : "https://example.invalid/assemblies/evil" ,
1086+ },
1087+ status_code = 200 ,
1088+ headers = {},
1089+ )
1090+ completed = Response (
1091+ data = {"ok" : "ASSEMBLY_COMPLETED" , "assembly_id" : "assembly-123" },
1092+ status_code = 200 ,
1093+ headers = {},
1094+ )
1095+
1096+ with mock .patch .object (client .request , "post" , new = mock .AsyncMock (return_value = initial )):
1097+ with mock .patch .object (
1098+ client ,
1099+ "get_assembly" ,
1100+ new = mock .AsyncMock (side_effect = [malicious_poll , completed ]),
1101+ ) as get_mock :
1102+ with mock .patch ("asyncio.sleep" , new_callable = mock .AsyncMock ):
1103+ response = await assembly .create (wait = True , resumable = False , retries = 2 )
1104+
1105+ self .assertEqual (response .data ["ok" ], "ASSEMBLY_COMPLETED" )
1106+ self .assertEqual (
1107+ get_mock .await_args_list ,
1108+ [
1109+ mock .call (assembly_url = initial_url ),
1110+ mock .call (assembly_url = initial_url ),
1111+ ],
1112+ )
1113+
10431114 async def test_async_assembly_wait_returns_last_poll_response_when_budget_exhausted (self ):
10441115 async with AsyncTransloadit ("key" , "secret" , service = self .server .base_url ) as client :
10451116 assembly = client .new_assembly ()
0 commit comments