Route browser fs and logs endpoints directly to the VM - #178
Conversation
Add fs and logs to the default direct-to-VM subresource prefixes so filesystem operations and log streaming use the cached browser base_url and JWT instead of the control plane. Extensions, replays and telemetry/events stay on the control plane.
The probe Request built for cache sniffing derives a multipart content-type that carries its own boundary. Copying that header onto the routed request while re-encoding the body produced a boundary mismatch, so the browser VM rejected direct fs.upload calls with "failed to read form part". Let the routed fetch derive the header when the body derives its own content-type.
Narrows the default prefix so a future logs read served by the control plane is not swept onto the browser VM.
rgarcia
left a comment
There was a problem hiding this comment.
reviewed alongside the go (#174) and python (#164) siblings, including a control-plane vs kernel-images surface comparison. all 16 fs/* and logs/stream endpoints match on params, body fields, 2xx codes and content types, and metro-api's /browser/kernel/* handler already wakes standby VMs and records session activity for direct requests, so the routing change itself looks safe.
the multipart content-type fix is correct and minimal: constructing the Request serialized the FormData once with boundary A, and the routed fetch re-serialized with boundary B while still sending header A. no previously routed endpoint had a multipart body, so this only surfaces with fs/upload / fs/upload_zip. the request.formData() test is the one that would have caught it. files[0][dest_path] naming is what the VM parser expects and matches what the control plane already forwarded.
Nits
src/lib/browser-routing.ts:314-319— theheaders.deleterelies onroutedInit.headerssharing the sameHeadersinstance; consider deleting before buildingroutedInitor a one-line notetests/lib/browser-routing.test.ts— fallback mocks return204forfs/upload/fs/write_file; production returns201
Sayan-
left a comment
There was a problem hiding this comment.
Approving. Verified against a mock control plane and VM over real HTTP: fs/* and logs/stream route to the VM with ?jwt= and no Authorization, telemetry/events and replays stay on the control plane with bearer auth, query strings and binary bodies survive intact, the env override pushes everything back, and a stale-JWT fallback replays a buffered body byte for byte and evicts the route.
-
p2: the stale-JWT replay tests inject
fetch, which cannot distinguish a safe replay from an unsafe one. Constructing aRequestfrom a drained stream inside an injected fetch yields an empty body and no error, so a body shape that fails at runtime still satisfies those assertions. Real undici throws instead. The routed-multipart boundary test does exercise the real encoder, so this applies only to the fallback-replay assertions. -
p2: a non-replayable body on the stale-JWT path surfaces
APIConnectionError: Connection error.against a healthy server. Python surfaces the original 401 and Go the failed request. Measured withfs.createReadStreamand an async generator. The route is evicted either way, so the next call recovers. -
p2, pre-existing: the
writeFileJSDoc example passesfs.createReadStream('path/to/file'), which is not assignable to the declaredcontentstype and is the shape that hits the above.
Summary
Adds
fsandlogs/streamto the defaultKERNEL_BROWSER_ROUTING_SUBRESOURCESprefixes, so every/browsers/{id}/fs/*operation (JSON, binary read/write, multipart upload, watch SSE) and/browsers/{id}/logs/streamis rewritten by the routing fetch to the browser VM's cachedbase_urlwith?jwt=and no API-keyAuthorization.Browser lifecycle/metadata, extensions, replays,
telemetry/events, and anything else underlogs/stay on the control plane.KERNEL_BROWSER_ROUTING_SUBRESOURCESstill overrides the list, and an empty value still disables routing.Fixes one latent bug the new routing surfaced:
createRoutingFetchbuilds a probeRequestfrom the incoming body for cache sniffing, and that construction derives amultipart/form-datacontent-type carrying its own boundary. Copying that header onto the routed request while re-encoding theFormDatabody produced a boundary mismatch, and the VM rejected directfs.uploadcalls with400 failed to read form part. The routed init now drops an inherited content-type when the body derives its own (FormData,URLSearchParams,Blob) and the caller did not set one explicitly.fs.uploadis the first routed multipart endpoint, so nothing previously routed was affected.Query preservation, header stripping, signal propagation, stale-JWT eviction and the fallback body construction are otherwise unchanged; the added tests pin the fallback's existing body replay behavior for the body shapes the SDK produces.
Tests
tests/lib/browser-routing.test.ts:fsandlogs/stream, and still excludetelemetry/events,fsx/...,logs,logs/history,logstream,extensions,replayslogs/streamroutes to the VM whilelogs,logs/historyandlogstreamstay on the API originlistFiles), binaryreadFile, binarywriteFile, multipartupload(indexedfiles[0][…]field names),fs/watch/{id}/eventsSSE andlogs/streamSSE route to the VM with the query preserved,?jwt=appended and noauthorizationheaderrequest.formData(), which fails on the boundary mismatch described above (verified: the test fails without the fix)logs.streamaborts stay wired to the routed request's signalwriteFilebinary body byte for byte and theuploadmultipart body on the control plane with bearer auth, and evicts the routetelemetry/events,replays,extensions) keep the API origin and bearer authRan locally:
jest(421 passed, 227 skipped),prettier --check .,eslint .,tsc,./scripts/build.Live validation
Ran against staging with real headless browsers:
fs.writeFile,fs.readFile,fs.listFiles,fs.upload(two entries),fs.watch.start/events/stopandlogs.streamall hithttps://<browser-host>/browser/kernel/...?jwt=...and returned the expected data, with uploaded files reading back with the correct per-entry contents.telemetry/eventsand the browser delete stayed on the control plane. Thefs.uploadfailure above was found this way and re-verified as fixed; re-validated after switching the default tologs/stream.Note
Medium Risk
Changes default request paths for fs and streaming logs (data and uploads) and touches request header/body handling for routed multipart; mitigated by control-plane fallback, env allowlist, and broad test coverage.
Overview
Default direct-to-VM routing now includes
fsandlogs/stream, so browser file operations (list/read/write/upload, watch SSE) and live log SSE go to the session VM with JWT query auth instead of the API control plane. Other paths underlogs/, plustelemetry/events, extensions, and replays, stay on the API origin.Multipart fix: When rebuilding the routed
fetchbody, inheritedcontent-typefrom an intermediateRequestcould disagree with a re-encodedFormDataboundary and breakfs.upload. Routed requests now dropcontent-typewhen the body isFormData/URLSearchParams/Bloband the caller did not set it explicitly, so the runtime derives a matching boundary.Tests cover prefix matching, VM routing for binary and multipart bodies, SSE abort wiring, stale-JWT fallback with body replay, and
KERNEL_BROWSER_ROUTING_SUBRESOURCESoverrides.Reviewed by Cursor Bugbot for commit dd066e9. Bugbot is set up for automated code reviews on this repo. Configure here.