@@ -50,6 +50,10 @@ def _skip_retry_sleep(_self: object, **_kwargs: object) -> None:
5050 return None
5151
5252
53+ async def _skip_async_retry_sleep (_self : object , ** _kwargs : object ) -> None :
54+ return None
55+
56+
5357class _UnseekableFile (io .RawIOBase ):
5458 """A file-like upload body that cannot be rewound, e.g. a pipe.
5559
@@ -1347,6 +1351,98 @@ async def handle_request(request: httpx.Request) -> httpx.Response:
13471351 assert requests [1 ].headers .get ("Authorization" ) == f"Bearer { api_key } "
13481352
13491353
1354+ def test_stale_direct_vm_auth_body_read_failure_does_not_retry_unreplayable_write (
1355+ monkeypatch : pytest .MonkeyPatch ,
1356+ ) -> None :
1357+ monkeypatch .delenv ("KERNEL_BROWSER_ROUTING_SUBRESOURCES" , raising = False )
1358+ monkeypatch .setattr ("kernel._base_client.SyncAPIClient._sleep_for_retry" , _skip_retry_sleep )
1359+ requests : list [tuple [httpx .URL , bytes ]] = []
1360+
1361+ def handle_request (request : httpx .Request ) -> httpx .Response :
1362+ body = b"" .join (request .stream )
1363+ requests .append ((request .url , body ))
1364+ if "browser-session.test" in str (request .url ):
1365+ return httpx .Response (401 , stream = _FailingSyncStream (), headers = {"content-type" : "text/plain" })
1366+ return httpx .Response (201 )
1367+
1368+ class Transport (httpx .BaseTransport ):
1369+ @override
1370+ def handle_request (self , request : httpx .Request ) -> httpx .Response :
1371+ return handle_request (request )
1372+
1373+ http_client = httpx .Client (transport = Transport ())
1374+ with Kernel (
1375+ base_url = base_url ,
1376+ api_key = api_key ,
1377+ http_client = http_client ,
1378+ _strict_response_validation = True ,
1379+ ) as client :
1380+ _cache_browser (client )
1381+ with pytest .raises (APIConnectionError ):
1382+ client .browsers .fs .write_file ("sess-1" , _UnseekableFile (b"payload" ), path = "/tmp/x" )
1383+
1384+ assert requests == [
1385+ (
1386+ httpx .URL ("http://browser-session.test/browser/kernel/fs/write_file?path=%2Ftmp%2Fx&jwt=token-abc" ),
1387+ b"payload" ,
1388+ )
1389+ ]
1390+ assert client .browser_route_cache .get ("sess-1" ) is None
1391+
1392+ client .browsers .fs .write_file ("sess-1" , b"next" , path = "/tmp/x" )
1393+
1394+ assert requests [1 ] == (httpx .URL (f"{ base_url } /browsers/sess-1/fs/write_file?path=%2Ftmp%2Fx" ), b"next" )
1395+
1396+
1397+ @pytest .mark .asyncio
1398+ async def test_async_stale_direct_vm_auth_body_read_failure_does_not_retry_unreplayable_write (
1399+ monkeypatch : pytest .MonkeyPatch ,
1400+ ) -> None :
1401+ monkeypatch .delenv ("KERNEL_BROWSER_ROUTING_SUBRESOURCES" , raising = False )
1402+ monkeypatch .setattr ("kernel._base_client.AsyncAPIClient._sleep_for_retry" , _skip_async_retry_sleep )
1403+ requests : list [tuple [httpx .URL , bytes ]] = []
1404+
1405+ async def handle_request (request : httpx .Request ) -> httpx .Response :
1406+ body = b"" .join ([chunk async for chunk in request .stream ])
1407+ requests .append ((request .url , body ))
1408+ if "browser-session.test" in str (request .url ):
1409+ return httpx .Response (403 , stream = _FailingAsyncStream (), headers = {"content-type" : "text/plain" })
1410+ return httpx .Response (201 )
1411+
1412+ async def payload () -> AsyncIterator [bytes ]:
1413+ yield b"payload"
1414+
1415+ class Transport (httpx .AsyncBaseTransport ):
1416+ @override
1417+ async def handle_async_request (self , request : httpx .Request ) -> httpx .Response :
1418+ return await handle_request (request )
1419+
1420+ http_client = httpx .AsyncClient (transport = Transport ())
1421+ async with AsyncKernel (
1422+ base_url = base_url ,
1423+ api_key = api_key ,
1424+ http_client = http_client ,
1425+ _strict_response_validation = True ,
1426+ ) as client :
1427+ route = browser_route_from_browser (_fake_browser ())
1428+ assert route is not None
1429+ client .browser_route_cache .set (route )
1430+ with pytest .raises (APIConnectionError ):
1431+ await client .browsers .fs .write_file ("sess-1" , cast (Any , payload ()), path = "/tmp/x" )
1432+
1433+ assert requests == [
1434+ (
1435+ httpx .URL ("http://browser-session.test/browser/kernel/fs/write_file?path=%2Ftmp%2Fx&jwt=token-abc" ),
1436+ b"payload" ,
1437+ )
1438+ ]
1439+ assert client .browser_route_cache .get ("sess-1" ) is None
1440+
1441+ await client .browsers .fs .write_file ("sess-1" , b"next" , path = "/tmp/x" )
1442+
1443+ assert requests [1 ] == (httpx .URL (f"{ base_url } /browsers/sess-1/fs/write_file?path=%2Ftmp%2Fx" ), b"next" )
1444+
1445+
13501446def test_copied_client_registers_one_route_eviction_hook () -> None :
13511447 with Kernel (base_url = base_url , api_key = api_key , _strict_response_validation = True ) as client :
13521448 copied = client .copy (api_key = "sk-456" )
0 commit comments