Skip to content

feat: add direct-to-vm browser routing#95

Merged
rgarcia merged 17 commits intonextfrom
raf/browser-scoped-client
Apr 24, 2026
Merged

feat: add direct-to-vm browser routing#95
rgarcia merged 17 commits intonextfrom
raf/browser-scoped-client

Conversation

@rgarcia
Copy link
Copy Markdown
Contributor

@rgarcia rgarcia commented Apr 13, 2026

Summary

  • replace the public browser-routing rollout config with KERNEL_BROWSER_ROUTING_SUBRESOURCES
  • default direct-to-VM subresource routing to curl when the env var is unset, and treat an explicit empty string as fully disabling browser subresource routing
  • keep browser session responses warming the shared BrowserRouteCache, which still powers Browsers.HTTPClient(sessionID) and cache-backed direct /curl/raw requests
  • keep the direct-to-VM request middleware internal to NewClient() so the SDK can grow toward automatic routing without exposing rollout knobs in the public API

Rollout behavior

  • env var unset: route only curl subresources directly to the browser VM
  • env var set to "": disable browser subresource routing entirely
  • env var set to a comma-separated list: route exactly those subresources directly to the browser VM
  • raw HTTP via Browsers.HTTPClient(sessionID) still always goes direct to the browser VM because it uses the cached browser route and /curl/raw

Test plan

  • go test ./...
  • KERNEL_API_KEY=... KERNEL_BASE_URL=https://api.onkernel.com go run ./examples/browser-routing

Note

Medium Risk
Adds a new request-rewriting middleware and cached route handling that changes how certain browser subresource calls are routed and how auth headers/query params are rewritten, which could impact request correctness. Behavior is gated by KERNEL_BROWSER_ROUTING_SUBRESOURCES but defaults to routing curl directly.

Overview
Enables direct-to-VM routing for selected browser subresource endpoints by introducing a shared BrowserRouteCache on Client and a DirectVMRoutingMiddleware that rewrites allowlisted /browsers/{session}/... requests to the session base_url, strips Authorization, and injects jwt when available.

Adds Browsers.HTTPClient(sessionID) to return an http.Client that always tunnels egress through the browser VM’s internal /curl/raw using cached route data, plus env-driven rollout via KERNEL_BROWSER_ROUTING_SUBRESOURCES (unset defaults to curl, empty disables routing).

Includes new lib/browserrouting implementation (route cache, websocket-jwt extraction, raw curl round-tripper), an example program, and unit/integration tests covering cache warm/evict, allowlist behavior, and request/response preservation.

Reviewed by Cursor Bugbot for commit 4f754d1. Bugbot is set up for automated code reviews on this repo. Configure here.

@firetiger-agent
Copy link
Copy Markdown

Firetiger deploy monitoring skipped

This PR didn't match the auto-monitor filter configured on your GitHub connection:

Any PR that changes the kernel API. Monitor changes to API endpoints (packages/api/cmd/api/) and Temporal workflows (packages/api/lib/temporal) in the kernel repo

Reason: PR modifies client libraries and session handling, not API endpoints (packages/api/cmd/api/) or Temporal workflows (packages/api/lib/temporal) as specified in the filter.

To monitor this PR anyway, reply with @firetiger monitor this.

Copy link
Copy Markdown

@cursor cursor Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Autofix Details

Bugbot Autofix prepared a fix for the issue found in the latest run.

  • ✅ Fixed: HTTPClient() never extracts user's custom HTTP client
    • Changed option.WithHTTPClient to return requestconfig.PreRequestOptionFunc so PreRequestOptions now applies it and BrowserSessionClient.HTTPClient() receives the caller’s custom client.

Create PR

Or push these changes by commenting:

@cursor push 3b2a924f02
Preview (3b2a924f02)
diff --git a/option/requestoption.go b/option/requestoption.go
--- a/option/requestoption.go
+++ b/option/requestoption.go
@@ -56,7 +56,7 @@
 // For custom uses cases, it is recommended to provide an [*http.Client] with a custom
 // [http.RoundTripper] as its transport, rather than directly implementing [HTTPClient].
 func WithHTTPClient(client HTTPClient) RequestOption {
-	return requestconfig.RequestOptionFunc(func(r *requestconfig.RequestConfig) error {
+	return requestconfig.PreRequestOptionFunc(func(r *requestconfig.RequestConfig) error {
 		if client == nil {
 			return fmt.Errorf("requestoption: custom http client cannot be nil")
 		}

You can send follow-ups to the cloud agent here.

Comment thread browser_session.go Outdated
Copy link
Copy Markdown

@cursor cursor Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Autofix Details

Bugbot Autofix prepared a fix for the issue found in the latest run.

  • ✅ Fixed: SDK-level dependency on code-gen-only golang.org/x/tools forces Go version bump
    • I moved internal/genbrowsersessionservices to its own Go module and updated generation entrypoints so golang.org/x/tools no longer affects the SDK module, allowing the root go.mod to stay at Go 1.22 without tool-only deps.

Create PR

Or push these changes by commenting:

@cursor push bac079904b
Preview (bac079904b)
diff --git a/browser_session.go b/browser_session.go
--- a/browser_session.go
+++ b/browser_session.go
@@ -1,6 +1,6 @@
 package kernel
 
-//go:generate go run ./internal/genbrowsersessionservices -output browser_session_services_gen.go
+//go:generate sh -c "cd internal/genbrowsersessionservices && go run . -dir ../.. -output browser_session_services_gen.go"
 
 import (
 	"fmt"

diff --git a/go.mod b/go.mod
--- a/go.mod
+++ b/go.mod
@@ -1,16 +1,13 @@
 module github.com/kernel/kernel-go-sdk
 
-go 1.23.0
+go 1.22
 
 require (
 	github.com/tidwall/gjson v1.18.0
 	github.com/tidwall/sjson v1.2.5
-	golang.org/x/tools v0.31.0
 )
 
 require (
 	github.com/tidwall/match v1.1.1 // indirect
-	github.com/tidwall/pretty v1.2.1 // indirect
-	golang.org/x/mod v0.24.0 // indirect
-	golang.org/x/sync v0.12.0 // indirect
+	github.com/tidwall/pretty v1.2.0 // indirect
 )

diff --git a/go.sum b/go.sum
--- a/go.sum
+++ b/go.sum
@@ -1,18 +1,9 @@
-github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
-github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
 github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
 github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY=
 github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
 github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
 github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
+github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs=
 github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
-github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4=
-github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
 github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY=
 github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28=
-golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU=
-golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
-golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw=
-golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
-golang.org/x/tools v0.31.0 h1:0EedkvKDbh+qistFTd0Bcwe/YLh4vHwWEkiI0toFIBU=
-golang.org/x/tools v0.31.0/go.mod h1:naFTU+Cev749tSJRXJlna0T3WxKvb1kWEx15xA4SdmQ=

diff --git a/internal/genbrowsersessionservices/go.mod b/internal/genbrowsersessionservices/go.mod
new file mode 100644
--- /dev/null
+++ b/internal/genbrowsersessionservices/go.mod
@@ -1,0 +1,10 @@
+module github.com/kernel/kernel-go-sdk/internal/genbrowsersessionservices
+
+go 1.23.0
+
+require golang.org/x/tools v0.31.0
+
+require (
+	golang.org/x/mod v0.24.0 // indirect
+	golang.org/x/sync v0.12.0 // indirect
+)

diff --git a/internal/genbrowsersessionservices/go.sum b/internal/genbrowsersessionservices/go.sum
new file mode 100644
--- /dev/null
+++ b/internal/genbrowsersessionservices/go.sum
@@ -1,0 +1,8 @@
+github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
+github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
+golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU=
+golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
+golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw=
+golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
+golang.org/x/tools v0.31.0 h1:0EedkvKDbh+qistFTd0Bcwe/YLh4vHwWEkiI0toFIBU=
+golang.org/x/tools v0.31.0/go.mod h1:naFTU+Cev749tSJRXJlna0T3WxKvb1kWEx15xA4SdmQ=

diff --git a/scripts/generate-browser-session b/scripts/generate-browser-session
--- a/scripts/generate-browser-session
+++ b/scripts/generate-browser-session
@@ -4,4 +4,7 @@
 
 cd "$(dirname "$0")/.."
 
-go run ./internal/genbrowsersessionservices -output browser_session_services_gen.go
+(
+  cd internal/genbrowsersessionservices
+  go run . -dir ../.. -output browser_session_services_gen.go
+)

You can send follow-ups to the cloud agent here.

Comment thread go.mod Outdated
rgarcia added 5 commits April 21, 2026 13:09
Bind browser subresource calls to a browser session's base_url and expose raw HTTP through a standard http.Client so metro-routed access feels like normal Go networking.

Made-with: Cursor
Use the browser session base_url directly for path rewriting, preserve custom HTTP clients in HTTPClient(), and add an env-gated integration test for browser-scoped routing.

Made-with: Cursor
Avoid depending on base_url path details in the integration test, keep the JWT helper package-private, and make round-tripper conformance explicit while preserving browser-scoped routing behavior.

Made-with: Cursor
Keep the raw round-tripper constructor package-private, remove defensive middleware branches that imply unsupported empty inputs, and retain the browser-scoped integration coverage without baking in base_url path details.

Made-with: Cursor
Replace the handwritten browser-scoped Go service façade with deterministic generated bindings derived from the generated browser service graph, and enforce regeneration in lint.

Made-with: Cursor
@rgarcia rgarcia force-pushed the raf/browser-scoped-client branch from 1720a28 to b6a77bc Compare April 21, 2026 17:09
Copy link
Copy Markdown

@cursor cursor Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

There are 2 total unresolved issues (including 1 from previous review).

Autofix Details

Bugbot Autofix prepared a fix for the issue found in the latest run.

  • ✅ Fixed: Middleware path rewrite ignores URL RawPath field
    • After rewriting req.URL.Path, the middleware now clears req.URL.RawPath so escaped-path requests consistently use the updated path, and a regression test covers this case.

Create PR

Or push these changes by commenting:

@cursor push 4794dcee9b
Preview (4794dcee9b)
diff --git a/lib/browserscope/middleware.go b/lib/browserscope/middleware.go
--- a/lib/browserscope/middleware.go
+++ b/lib/browserscope/middleware.go
@@ -33,6 +33,7 @@
 					rest = "/" + rest
 				}
 				req.URL.Path = prefix + rest
+				req.URL.RawPath = ""
 			}
 		}
 

diff --git a/lib/browserscope/middleware_test.go b/lib/browserscope/middleware_test.go
--- a/lib/browserscope/middleware_test.go
+++ b/lib/browserscope/middleware_test.go
@@ -55,3 +55,27 @@
 func TestBrowserSessionMiddlewareType(t *testing.T) {
 	var _ option.Middleware = BrowserSessionMiddleware("a", "b")
 }
+
+func TestBrowserSessionMiddlewareClearsRawPathOnRewrite(t *testing.T) {
+	mw := BrowserSessionMiddleware("sess1", "")
+	var final *http.Request
+	next := func(req *http.Request) (*http.Response, error) {
+		final = req
+		return nil, nil
+	}
+
+	u, err := url.Parse("https://host/browser/kernel/browsers/sess1/process/%20exec")
+	if err != nil {
+		t.Fatal(err)
+	}
+	req := &http.Request{URL: u}
+
+	_, _ = mw(req, next)
+
+	if final.URL.RawPath != "" {
+		t.Fatalf("raw path should be cleared after rewrite: got %q", final.URL.RawPath)
+	}
+	if got := final.URL.EscapedPath(); got != "/browser/kernel/process/%20exec" {
+		t.Fatalf("escaped path rewrite: got %q", got)
+	}
+}

You can send follow-ups to the cloud agent here.

Comment thread lib/browserscope/middleware.go Outdated
rgarcia added 2 commits April 21, 2026 16:24
Show the browser-scoped HTTPClient flow explicitly so the /curl/raw-backed public API is discoverable from a runnable Go example.

Made-with: Cursor
Route browser subresources and raw HTTP through the shared browser route cache so the SDK no longer needs the generated browser-scoped client surface.

Made-with: Cursor
Copy link
Copy Markdown

@cursor cursor Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 2 potential issues.

There are 4 total unresolved issues (including 2 from previous reviews).

Autofix Details

Bugbot Autofix prepared fixes for both issues found in the latest run.

  • ✅ Fixed: Error from PreRequestOptions silently swallowed in HTTPClient
    • BrowserService.HTTPClient now returns nil, err when requestconfig.PreRequestOptions fails, and a regression test verifies the error is propagated.
  • ✅ Fixed: NewClient mutates shared option causing cross-client cache corruption
    • NewClient now clones each browserRoutingOption before injecting the per-client cache so reused options no longer share mutable cache pointers, with a test confirming per-client cache isolation.

Create PR

Or push these changes by commenting:

@cursor push 2f05d40ea0
Preview (2f05d40ea0)
diff --git a/browser.go b/browser.go
--- a/browser.go
+++ b/browser.go
@@ -174,7 +174,7 @@
 	}
 	cfg, err := requestconfig.PreRequestOptions(opts...)
 	if err != nil {
-		return browserscope.HTTPClient(route.BaseURL, route.JWT, nil), nil
+		return nil, err
 	}
 	return browserscope.HTTPClient(route.BaseURL, route.JWT, cfg.HTTPClient), nil
 }

diff --git a/browser_routing_test.go b/browser_routing_test.go
--- a/browser_routing_test.go
+++ b/browser_routing_test.go
@@ -111,3 +111,17 @@
 		t.Fatalf("expected control-plane path, got %q", got)
 	}
 }
+
+func TestBrowserRoutingOptionReuseKeepsPerClientRouteCache(t *testing.T) {
+	routingOpt := WithBrowserRouting(BrowserRoutingConfig{Enabled: true, DirectToVMSubresources: []string{"process"}})
+
+	client1 := NewClient(routingOpt)
+	client2 := NewClient(routingOpt)
+
+	if got := browserRouteCacheFromOptions(client1.Options); got != client1.BrowserRouteCache {
+		t.Fatalf("expected client1 options to resolve client1 cache, got %p want %p", got, client1.BrowserRouteCache)
+	}
+	if got := browserRouteCacheFromOptions(client2.Options); got != client2.BrowserRouteCache {
+		t.Fatalf("expected client2 options to resolve client2 cache, got %p want %p", got, client2.BrowserRouteCache)
+	}
+}

diff --git a/browser_session_httpclient_test.go b/browser_session_httpclient_test.go
--- a/browser_session_httpclient_test.go
+++ b/browser_session_httpclient_test.go
@@ -1,11 +1,13 @@
 package kernel
 
 import (
+	"errors"
 	"io"
 	"net/http"
 	"net/http/httptest"
 	"testing"
 
+	"github.com/kernel/kernel-go-sdk/internal/requestconfig"
 	"github.com/kernel/kernel-go-sdk/lib/browserscope"
 	"github.com/kernel/kernel-go-sdk/option"
 )
@@ -75,3 +77,30 @@
 		t.Fatal("expected cached route lookup failure")
 	}
 }
+
+func TestBrowserSessionHTTPClientPropagatesPreRequestOptionError(t *testing.T) {
+	c := NewClient(
+		option.WithBaseURL("https://api.example/"),
+		option.WithAPIKey("sk"),
+	)
+
+	primeBrowserRouteCache(c.Options, browserscope.Ref{
+		SessionID: "sid",
+		BaseURL:   "https://browser-session.test/browser/kernel",
+		CdpWsURL:  "wss://x/browser/cdp?jwt=j1",
+	})
+
+	wantErr := errors.New("pre-request failed")
+	client, err := c.Browsers.HTTPClient(
+		"sid",
+		requestconfig.PreRequestOptionFunc(func(*requestconfig.RequestConfig) error {
+			return wantErr
+		}),
+	)
+	if !errors.Is(err, wantErr) {
+		t.Fatalf("expected error %q, got %v", wantErr, err)
+	}
+	if client != nil {
+		t.Fatal("expected nil client when pre-request options fail")
+	}
+}

diff --git a/client.go b/client.go
--- a/client.go
+++ b/client.go
@@ -65,9 +65,11 @@
 func NewClient(opts ...option.RequestOption) (r Client) {
 	opts = append(DefaultClientOptions(), opts...)
 	cache := browserscope.NewRouteCache()
-	for _, opt := range opts {
+	for i, opt := range opts {
 		if routing, ok := opt.(*browserRoutingOption); ok {
-			routing.cache = cache
+			routingWithCache := *routing
+			routingWithCache.cache = cache
+			opts[i] = &routingWithCache
 		}
 	}
 	opts = append(opts, withBrowserRouteCache(cache))

You can send follow-ups to the cloud agent here.

Comment thread browser.go Outdated
Comment thread client.go Outdated
Drop the extra cache priming helper, remove metro wording, and rename the example so the go diff stays focused on direct-to-VM routing.

Made-with: Cursor
@rgarcia rgarcia changed the title feat: add browser-scoped session client feat: add direct-to-vm browser routing Apr 22, 2026
rgarcia added 3 commits April 22, 2026 12:57
Rename the browser routing allowlist field to Subresources so the direct-to-VM configuration is shorter and easier to read.

Made-with: Cursor
Rename the handwritten routing helpers to browserrouting, fix the shared-cache and RawPath issues, and revert the generated/module churn that should not stay in the PR.

Made-with: Cursor
Drop the superseded lib/browserscope files now that the renamed browserrouting package owns the direct-to-VM helpers.

Made-with: Cursor
Copy link
Copy Markdown

@cursor cursor Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Autofix Details

Bugbot Autofix prepared a fix for the issue found in the latest run.

  • ✅ Fixed: Config error silently swallowed, dropping custom HTTP client
    • The browser HTTP client constructor now returns the NewRequestConfig error instead of silently falling back to a default client.

Create PR

Or push these changes by commenting:

@cursor push 8a33f5475d
Preview (8a33f5475d)
diff --git a/browser_http_client.go b/browser_http_client.go
--- a/browser_http_client.go
+++ b/browser_http_client.go
@@ -27,7 +27,7 @@
 
 	cfg, err := requestconfig.NewRequestConfig(context.Background(), http.MethodGet, "https://example.com", nil, nil, opts...)
 	if err != nil {
-		return browserrouting.NewHTTPClient(route.BaseURL, route.JWT, nil), nil
+		return nil, err
 	}
 
 	return browserrouting.NewHTTPClient(route.BaseURL, route.JWT, cfg.HTTPClient), nil

You can send follow-ups to the cloud agent here.

Comment thread browser_http_client.go
rgarcia added 3 commits April 24, 2026 11:08
Stop exposing browser routing rollout controls on the client constructor and derive direct-to-VM subresource routing from KERNEL_BROWSER_ROUTING_SUBRESOURCES instead, defaulting to curl while keeping raw HTTP helpers cache-backed.
Return request config errors from Browsers.HTTPClient instead of silently falling back to the default client, so invalid options do not drop custom HTTP behavior without notice. Add a regression test for the failure path.

Made-with: Cursor
Centralize browser route cache warm-up and eviction in the shared routing middleware so browser service methods can stay generic while direct-to-VM routing still learns browser base URLs from API responses.

Made-with: Cursor
Copy link
Copy Markdown

@cursor cursor Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 2 potential issues.

Autofix Details

Bugbot Autofix prepared fixes for both issues found in the latest run.

  • ✅ Fixed: Missing default case causes incorrect path scanning
    • I added a default branch in parseBrowserMetadataPath to immediately return false for subresource paths after the first browsers segment.
  • ✅ Fixed: Duplicated dead post-response code in middleware
    • I removed the duplicated post-response block by unifying response handling into a single shared section after optional direct-VM rewrite logic.

Create PR

Or push these changes by commenting:

@cursor push 8ec9b4a5bd
Preview (8ec9b4a5bd)
diff --git a/lib/browserrouting/route_cache.go b/lib/browserrouting/route_cache.go
--- a/lib/browserrouting/route_cache.go
+++ b/lib/browserrouting/route_cache.go
@@ -79,42 +79,29 @@
 	return func(req *http.Request, next option.MiddlewareNext) (*http.Response, error) {
 		cacheSessionID, cacheablePath := parseBrowserMetadataPath(req.URL.Path)
 		sessionID, subresource, suffix, ok := parseDirectVMPath(req.URL.Path)
-		if !ok {
-			res, err := next(req)
-			if err != nil {
-				return res, err
-			}
-			if req.Method == http.MethodDelete && cacheSessionID != "" && isSuccessfulResponse(res) {
-				cache.Delete(cacheSessionID)
-			}
-			if cacheablePath {
-				if err := sniffAndPopulateCache(res, cache); err != nil {
-					return nil, err
-				}
-			}
-			return res, nil
-		}
-		if _, ok := allowed[subresource]; ok {
-			route, ok := cache.Load(sessionID)
-			if ok {
-				base, err := url.Parse(route.BaseURL)
-				if err != nil {
-					return nil, err
-				}
-				req.Header.Del("Authorization")
-				if route.JWT != "" {
-					q := req.URL.Query()
-					if q.Get("jwt") == "" {
-						q.Set("jwt", route.JWT)
-						req.URL.RawQuery = q.Encode()
+		if ok {
+			if _, ok := allowed[subresource]; ok {
+				route, ok := cache.Load(sessionID)
+				if ok {
+					base, err := url.Parse(route.BaseURL)
+					if err != nil {
+						return nil, err
 					}
+					req.Header.Del("Authorization")
+					if route.JWT != "" {
+						q := req.URL.Query()
+						if q.Get("jwt") == "" {
+							q.Set("jwt", route.JWT)
+							req.URL.RawQuery = q.Encode()
+						}
+					}
+
+					req.URL.Scheme = base.Scheme
+					req.URL.Host = base.Host
+					req.Host = base.Host
+					req.URL.Path = joinURLPath(base.Path, subresource, suffix)
+					req.URL.RawPath = ""
 				}
-
-				req.URL.Scheme = base.Scheme
-				req.URL.Host = base.Host
-				req.Host = base.Host
-				req.URL.Path = joinURLPath(base.Path, subresource, suffix)
-				req.URL.RawPath = ""
 			}
 		}
 
@@ -148,6 +135,8 @@
 				return "", false
 			}
 			return parts[i+1], true
+		default:
+			return "", false
 		}
 	}
 	return "", false

You can send follow-ups to the cloud agent here.

Comment thread lib/browserrouting/route_cache.go
Comment thread lib/browserrouting/route_cache.go Outdated
Reject subresource paths when detecting browser metadata responses and unify the post-response cache sync path so routing middleware does not carry dead duplicate logic.

Made-with: Cursor
Copy link
Copy Markdown

@cursor cursor Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

Bugbot Autofix prepared a fix for the issue found in the latest run.

  • ✅ Fixed: Cache delete then sniff ordering re-adds deleted route
    • Reordered finalizeResponse to sniff/populate first and perform successful DELETE eviction afterward so deleted sessions cannot be re-cached from response bodies.

Create PR

Or push these changes by commenting:

@cursor push dbaa41f099
Preview (dbaa41f099)
diff --git a/lib/browserrouting/route_cache.go b/lib/browserrouting/route_cache.go
--- a/lib/browserrouting/route_cache.go
+++ b/lib/browserrouting/route_cache.go
@@ -135,14 +135,14 @@
 }
 
 func finalizeResponse(req *http.Request, res *http.Response, cache *RouteCache, cacheSessionID string, cacheablePath bool) (*http.Response, error) {
-	if req.Method == http.MethodDelete && cacheSessionID != "" && isSuccessfulResponse(res) {
-		cache.Delete(cacheSessionID)
-	}
 	if cacheablePath {
 		if err := sniffAndPopulateCache(res, cache); err != nil {
 			return nil, err
 		}
 	}
+	if req.Method == http.MethodDelete && cacheSessionID != "" && isSuccessfulResponse(res) {
+		cache.Delete(cacheSessionID)
+	}
 	return res, nil
 }

You can send follow-ups to the cloud agent here.

Reviewed by Cursor Bugbot for commit b293866. Configure here.

Comment thread lib/browserrouting/route_cache.go
Process cache sniffing before successful browser delete eviction so delete responses that include browser metadata cannot reinsert stale route entries. Add a regression test for JSON delete responses.

Made-with: Cursor
Copy link
Copy Markdown
Contributor

@Sayan- Sayan- left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

great stuff - thanks for iterating on the shape!

Warm the direct VM route cache from browser pool acquire responses and evict released sessions by sniffing the pool release request body in middleware.

Made-with: Cursor
Copy link
Copy Markdown
Contributor

@Sayan- Sayan- left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

great stuff - thanks for iterating on the shape!

@rgarcia rgarcia merged commit a13c8f6 into next Apr 24, 2026
7 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants