Skip to content

Commit 3037090

Browse files
committed
Evict the originating browser route cache
1 parent 1a3bbc3 commit 3037090

3 files changed

Lines changed: 123 additions & 7 deletions

File tree

src/kernel/_client.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -378,7 +378,7 @@ def _prepare_options(self, options: Any) -> Any:
378378

379379
@override
380380
def _prepare_request(self, request: httpx.Request) -> None:
381-
prepare_direct_vm_request(request)
381+
prepare_direct_vm_request(request, cache=self.browser_route_cache)
382382

383383
@override
384384
def _should_retry_on_connection_error(self, request: httpx.Request) -> bool:
@@ -777,7 +777,7 @@ async def _prepare_options(self, options: Any) -> Any:
777777

778778
@override
779779
async def _prepare_request(self, request: httpx.Request) -> None:
780-
prepare_direct_vm_request(request)
780+
prepare_direct_vm_request(request, cache=self.browser_route_cache)
781781

782782
@override
783783
def _should_retry_on_connection_error(self, request: httpx.Request) -> bool:

src/kernel/lib/browser_routing/routing.py

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ class BrowserRoutingConfig:
3838
_DIRECT_VM_REQUEST_MARKER_HEADER = "x-kernel-direct-vm-request"
3939
_STALE_DIRECT_VM_AUTH_REQUEST_EXTENSION = "kernel_stale_direct_vm_auth"
4040
_DIRECT_VM_BODY_REPLAYABLE_REQUEST_EXTENSION = "kernel_direct_vm_body_replayable"
41+
_DIRECT_VM_ROUTE_CACHE_REQUEST_EXTENSION = "kernel_direct_vm_route_cache"
4142

4243

4344
_BROWSER_ROUTE_CACHEABLE_PATH = re.compile(r"^/(?:v\d+/)?browsers(?:/[^/]+)?/?$")
@@ -225,10 +226,11 @@ def install_stale_direct_vm_auth_eviction(client: httpx.Client, *, cache: Browse
225226
return
226227

227228
def handle_response(response: httpx.Response) -> None:
228-
_reject_unreplayable_direct_vm_redirect(response, cache=cache)
229+
request_cache = _direct_vm_route_cache(response.request) or cache
230+
_reject_unreplayable_direct_vm_redirect(response, cache=request_cache)
229231
if is_stale_direct_vm_auth_response(response):
230232
response.request.extensions[_STALE_DIRECT_VM_AUTH_REQUEST_EXTENSION] = True
231-
maybe_evict_browser_route_from_response(response, cache=cache)
233+
maybe_evict_browser_route_from_response(response, cache=request_cache)
232234

233235
setattr(handle_response, _EVICTION_HOOK_CACHE_ATTR, cache)
234236
hooks.insert(0, handle_response)
@@ -241,10 +243,11 @@ def install_async_stale_direct_vm_auth_eviction(client: httpx.AsyncClient, *, ca
241243
return
242244

243245
async def handle_response(response: httpx.Response) -> None:
244-
_reject_unreplayable_direct_vm_redirect(response, cache=cache)
246+
request_cache = _direct_vm_route_cache(response.request) or cache
247+
_reject_unreplayable_direct_vm_redirect(response, cache=request_cache)
245248
if is_stale_direct_vm_auth_response(response):
246249
response.request.extensions[_STALE_DIRECT_VM_AUTH_REQUEST_EXTENSION] = True
247-
maybe_evict_browser_route_from_response(response, cache=cache)
250+
maybe_evict_browser_route_from_response(response, cache=request_cache)
248251

249252
setattr(handle_response, _EVICTION_HOOK_CACHE_ATTR, cache)
250253
hooks.insert(0, handle_response)
@@ -466,7 +469,7 @@ def rewrite_direct_vm_options(
466469
return rewritten
467470

468471

469-
def prepare_direct_vm_request(request: httpx.Request) -> None:
472+
def prepare_direct_vm_request(request: httpx.Request, *, cache: BrowserRouteCache) -> None:
470473
if request.headers.pop(_DIRECT_VM_REQUEST_MARKER_HEADER, None) is None:
471474
return
472475

@@ -475,9 +478,15 @@ def prepare_direct_vm_request(request: httpx.Request) -> None:
475478
request.extensions[_DIRECT_VM_BODY_REPLAYABLE_REQUEST_EXTENSION] = _classify_direct_vm_request_body_replayability(
476479
request
477480
)
481+
request.extensions[_DIRECT_VM_ROUTE_CACHE_REQUEST_EXTENSION] = cache
478482
request.headers.pop("Authorization", None)
479483

480484

485+
def _direct_vm_route_cache(request: httpx.Request) -> BrowserRouteCache | None:
486+
cache = request.extensions.get(_DIRECT_VM_ROUTE_CACHE_REQUEST_EXTENSION)
487+
return cache if isinstance(cache, BrowserRouteCache) else None
488+
489+
481490
def _is_direct_vm_request(request: httpx.Request) -> bool:
482491
return isinstance(request.extensions.get(_DIRECT_VM_BODY_REPLAYABLE_REQUEST_EXTENSION), bool)
483492

tests/test_browser_routing.py

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1726,6 +1726,113 @@ async def handle_async_request(self, request: httpx.Request) -> httpx.Response:
17261726
]
17271727

17281728

1729+
@pytest.mark.parametrize("origin_first", [True, False])
1730+
def test_direct_vm_redirect_evicts_originating_cache_when_http_client_is_shared(
1731+
monkeypatch: pytest.MonkeyPatch,
1732+
origin_first: bool,
1733+
) -> None:
1734+
monkeypatch.delenv("KERNEL_BROWSER_ROUTING_SUBRESOURCES", raising=False)
1735+
requests: list[tuple[httpx.URL, bytes]] = []
1736+
1737+
class Transport(httpx.BaseTransport):
1738+
@override
1739+
def handle_request(self, request: httpx.Request) -> httpx.Response:
1740+
body = b"".join(cast(Iterator[bytes], request.stream))
1741+
requests.append((request.url, body))
1742+
return httpx.Response(
1743+
307,
1744+
headers={"location": "http://other-vm.test/browser/kernel/fs/write_file"},
1745+
)
1746+
1747+
http_client = httpx.Client(transport=Transport(), follow_redirects=True)
1748+
1749+
def make_client() -> Kernel:
1750+
return Kernel(
1751+
base_url=base_url,
1752+
api_key=api_key,
1753+
http_client=http_client,
1754+
_strict_response_validation=True,
1755+
)
1756+
1757+
if origin_first:
1758+
client, other_client = make_client(), make_client()
1759+
else:
1760+
other_client, client = make_client(), make_client()
1761+
1762+
try:
1763+
_cache_browser(client)
1764+
with pytest.raises(APIConnectionError):
1765+
client.browsers.fs.write_file("sess-1", _UnseekableFile(b"payload"), path="/tmp/x")
1766+
assert client.browser_route_cache.get("sess-1") is None
1767+
assert other_client.browser_route_cache.get("sess-1") is None
1768+
finally:
1769+
http_client.close()
1770+
1771+
assert requests == [
1772+
(
1773+
httpx.URL("http://browser-session.test/browser/kernel/fs/write_file?path=%2Ftmp%2Fx&jwt=token-abc"),
1774+
b"payload",
1775+
)
1776+
]
1777+
1778+
1779+
@pytest.mark.asyncio
1780+
@pytest.mark.parametrize("origin_first", [True, False])
1781+
async def test_async_direct_vm_redirect_evicts_originating_cache_when_http_client_is_shared(
1782+
monkeypatch: pytest.MonkeyPatch,
1783+
origin_first: bool,
1784+
) -> None:
1785+
monkeypatch.delenv("KERNEL_BROWSER_ROUTING_SUBRESOURCES", raising=False)
1786+
requests: list[tuple[httpx.URL, bytes]] = []
1787+
1788+
class Transport(httpx.AsyncBaseTransport):
1789+
@override
1790+
async def handle_async_request(self, request: httpx.Request) -> httpx.Response:
1791+
body = b"".join([chunk async for chunk in cast(AsyncIterator[bytes], request.stream)])
1792+
requests.append((request.url, body))
1793+
return httpx.Response(
1794+
308,
1795+
headers={"location": "http://other-vm.test/browser/kernel/fs/write_file"},
1796+
)
1797+
1798+
http_client = httpx.AsyncClient(transport=Transport(), follow_redirects=True)
1799+
1800+
def make_client() -> AsyncKernel:
1801+
return AsyncKernel(
1802+
base_url=base_url,
1803+
api_key=api_key,
1804+
http_client=http_client,
1805+
_strict_response_validation=True,
1806+
)
1807+
1808+
if origin_first:
1809+
client, other_client = make_client(), make_client()
1810+
else:
1811+
other_client, client = make_client(), make_client()
1812+
1813+
try:
1814+
route = browser_route_from_browser(_fake_browser())
1815+
assert route is not None
1816+
client.browser_route_cache.set(route)
1817+
with pytest.raises(APIConnectionError):
1818+
await client.browsers.fs.write_file(
1819+
"sess-1",
1820+
cast(Any, _UnreplayableAsyncBody(b"payload")),
1821+
path="/tmp/x",
1822+
)
1823+
assert client.browser_route_cache.get("sess-1") is None
1824+
assert other_client.browser_route_cache.get("sess-1") is None
1825+
finally:
1826+
await http_client.aclose()
1827+
1828+
assert requests == [
1829+
(
1830+
httpx.URL("http://browser-session.test/browser/kernel/fs/write_file?path=%2Ftmp%2Fx&jwt=token-abc"),
1831+
b"payload",
1832+
)
1833+
]
1834+
1835+
17291836
def test_direct_vm_auth_is_removed_after_httpx_and_request_hooks(monkeypatch: pytest.MonkeyPatch) -> None:
17301837
monkeypatch.delenv("KERNEL_BROWSER_ROUTING_SUBRESOURCES", raising=False)
17311838
requests: list[httpx.Request] = []

0 commit comments

Comments
 (0)