Skip to content

Commit 94c0e23

Browse files
committed
Evict stale routes from browser streams
1 parent ba94ef3 commit 94c0e23

3 files changed

Lines changed: 69 additions & 2 deletions

File tree

src/kernel/lib/browser_routing/raw_http.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
import httpx
88

99
from .util import sanitize_curl_raw_params
10-
from .routing import BrowserRoute, mark_direct_vm_headers
10+
from .routing import BrowserRoute, mark_direct_vm_headers, direct_vm_request_extensions
1111
from ..._types import Body, Timeout, NotGiven, not_given
1212
from ..._models import FinalRequestOptions
1313

@@ -70,6 +70,7 @@ def stream_via_browser_route(
7070
headers=request_headers,
7171
content=_normalize_binary_content(content),
7272
timeout=_normalize_timeout(effective_timeout),
73+
extensions=direct_vm_request_extensions(cache=parent.browser_route_cache),
7374
) as response:
7475
yield response
7576

@@ -130,6 +131,7 @@ async def async_stream_via_browser_route(
130131
headers=request_headers,
131132
content=_normalize_binary_content(content),
132133
timeout=_normalize_timeout(effective_timeout),
134+
extensions=direct_vm_request_extensions(cache=parent.browser_route_cache),
133135
) as response:
134136
yield response
135137

src/kernel/lib/browser_routing/routing.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -491,8 +491,14 @@ def _direct_vm_route_cache(request: httpx.Request) -> BrowserRouteCache | None:
491491
return cache if isinstance(cache, BrowserRouteCache) else None
492492

493493

494+
def direct_vm_request_extensions(*, cache: BrowserRouteCache) -> dict[str, object]:
495+
return {_DIRECT_VM_ROUTE_CACHE_REQUEST_EXTENSION: cache}
496+
497+
494498
def _is_direct_vm_request(request: httpx.Request) -> bool:
495-
return isinstance(request.extensions.get(_DIRECT_VM_BODY_REPLAYABLE_REQUEST_EXTENSION), bool)
499+
return _direct_vm_route_cache(request) is not None or isinstance(
500+
request.extensions.get(_DIRECT_VM_BODY_REPLAYABLE_REQUEST_EXTENSION), bool
501+
)
496502

497503

498504
def match_direct_vm_path(path: str) -> tuple[str, str, str] | None:

tests/test_browser_routing.py

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,65 @@ def test_browser_request_uses_curl_raw() -> None:
194194
assert request.headers.get("x-kernel-direct-vm-request") is None
195195

196196

197+
def test_browser_stream_evicts_stale_route() -> None:
198+
requests: list[httpx.Request] = []
199+
200+
def handle_request(request: httpx.Request) -> httpx.Response:
201+
requests.append(request)
202+
if request.url.host == "browser-session.test":
203+
return httpx.Response(401)
204+
return httpx.Response(201)
205+
206+
http_client = httpx.Client(transport=httpx.MockTransport(handle_request))
207+
with Kernel(
208+
base_url=base_url,
209+
api_key=api_key,
210+
http_client=http_client,
211+
_strict_response_validation=True,
212+
) as client:
213+
_cache_browser(client)
214+
with client.browsers.stream("sess-1", "POST", "https://example.com", content=[b"payload"]) as response:
215+
assert response.status_code == 401
216+
assert client.browser_route_cache.get("sess-1") is None
217+
client.browsers.fs.write_file("sess-1", b"next", path="/tmp/x")
218+
219+
assert requests[0].url.host == "browser-session.test"
220+
assert requests[0].headers.get("Authorization") is None
221+
assert requests[1].url == httpx.URL(f"{base_url}/browsers/sess-1/fs/write_file?path=%2Ftmp%2Fx")
222+
assert requests[1].headers.get("Authorization") == f"Bearer {api_key}"
223+
224+
225+
@pytest.mark.asyncio
226+
async def test_async_browser_stream_evicts_stale_route() -> None:
227+
requests: list[httpx.Request] = []
228+
229+
async def handle_request(request: httpx.Request) -> httpx.Response:
230+
requests.append(request)
231+
if request.url.host == "browser-session.test":
232+
return httpx.Response(403)
233+
return httpx.Response(201)
234+
235+
http_client = httpx.AsyncClient(transport=httpx.MockTransport(handle_request))
236+
async with AsyncKernel(
237+
base_url=base_url,
238+
api_key=api_key,
239+
http_client=http_client,
240+
_strict_response_validation=True,
241+
) as client:
242+
route = browser_route_from_browser(_fake_browser())
243+
assert route is not None
244+
client.browser_route_cache.set(route)
245+
async with client.browsers.stream("sess-1", "POST", "https://example.com", content=b"payload") as response:
246+
assert response.status_code == 403
247+
assert client.browser_route_cache.get("sess-1") is None
248+
await client.browsers.fs.write_file("sess-1", b"next", path="/tmp/x")
249+
250+
assert requests[0].url.host == "browser-session.test"
251+
assert requests[0].headers.get("Authorization") is None
252+
assert requests[1].url == httpx.URL(f"{base_url}/browsers/sess-1/fs/write_file?path=%2Ftmp%2Fx")
253+
assert requests[1].headers.get("Authorization") == f"Bearer {api_key}"
254+
255+
197256
@respx.mock
198257
def test_telemetry_stream_routes_directly_to_vm(monkeypatch: pytest.MonkeyPatch) -> None:
199258
monkeypatch.setenv("KERNEL_BROWSER_ROUTING_SUBRESOURCES", "telemetry/stream")

0 commit comments

Comments
 (0)