Skip to content

Commit c62b34c

Browse files
committed
feat(cdp-proxy): add /json endpoint for Playwright connectOverCDP support
The CDP proxy on port 9222 previously only implemented /json/version, which returned the WebSocket URL but didn't support target discovery. Playwright's connectOverCDP() fetches /json to discover browser targets before establishing a WebSocket connection. Without this endpoint, using `http://127.0.0.1:9222` with agent-browser or Playwright would fail, even though direct WebSocket connections (ws://127.0.0.1:9222) worked fine. This change: - Adds /json and /json/list endpoints that proxy to Chrome's /json - Rewrites webSocketDebuggerUrl and devtoolsFrontendUrl in the response to use the proxy's host instead of Chrome's internal host - Enables `agent-browser --cdp http://127.0.0.1:9222` to work correctly
1 parent ed8d06e commit c62b34c

File tree

1 file changed

+71
-3
lines changed

1 file changed

+71
-3
lines changed

server/cmd/api/main.go

Lines changed: 71 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -157,9 +157,9 @@ func main() {
157157
},
158158
scaletozero.Middleware(stz),
159159
)
160-
// Expose a minimal /json/version endpoint so clients that attempt to
161-
// resolve a browser websocket URL via HTTP can succeed. We map the
162-
// upstream path onto this proxy's host:port so clients connect back to us.
160+
// Expose /json/version endpoint so clients that attempt to resolve a browser
161+
// websocket URL via HTTP can succeed. We map the upstream path onto this
162+
// proxy's host:port so clients connect back to us.
163163
rDevtools.Get("/json/version", func(w http.ResponseWriter, r *http.Request) {
164164
current := upstreamMgr.Current()
165165
if current == "" {
@@ -172,6 +172,61 @@ func main() {
172172
"webSocketDebuggerUrl": proxyWSURL,
173173
})
174174
})
175+
176+
// Handler for /json and /json/list - proxies to Chrome and rewrites URLs.
177+
// This is needed for Playwright's connectOverCDP which fetches /json for target discovery.
178+
jsonTargetHandler := func(w http.ResponseWriter, r *http.Request) {
179+
current := upstreamMgr.Current()
180+
if current == "" {
181+
http.Error(w, "upstream not ready", http.StatusServiceUnavailable)
182+
return
183+
}
184+
185+
// Parse upstream URL to get Chrome's host (e.g., ws://127.0.0.1:9223/...)
186+
parsed, err := url.Parse(current)
187+
if err != nil {
188+
http.Error(w, "invalid upstream URL", http.StatusInternalServerError)
189+
return
190+
}
191+
192+
// Fetch /json from Chrome
193+
chromeJSONURL := fmt.Sprintf("http://%s/json", parsed.Host)
194+
resp, err := http.Get(chromeJSONURL)
195+
if err != nil {
196+
slogger.Error("failed to fetch /json from Chrome", "err", err, "url", chromeJSONURL)
197+
http.Error(w, "failed to fetch target list from browser", http.StatusBadGateway)
198+
return
199+
}
200+
defer resp.Body.Close()
201+
202+
// Read and parse the JSON response
203+
var targets []map[string]interface{}
204+
if err := json.NewDecoder(resp.Body).Decode(&targets); err != nil {
205+
slogger.Error("failed to decode /json response", "err", err)
206+
http.Error(w, "failed to parse target list", http.StatusBadGateway)
207+
return
208+
}
209+
210+
// Rewrite URLs to use this proxy's host instead of Chrome's
211+
proxyHost := r.Host
212+
chromeHost := parsed.Host
213+
for i := range targets {
214+
// Rewrite webSocketDebuggerUrl
215+
if wsURL, ok := targets[i]["webSocketDebuggerUrl"].(string); ok {
216+
targets[i]["webSocketDebuggerUrl"] = rewriteWSURL(wsURL, chromeHost, proxyHost)
217+
}
218+
// Rewrite devtoolsFrontendUrl if present
219+
if frontendURL, ok := targets[i]["devtoolsFrontendUrl"].(string); ok {
220+
targets[i]["devtoolsFrontendUrl"] = rewriteWSURL(frontendURL, chromeHost, proxyHost)
221+
}
222+
}
223+
224+
w.Header().Set("Content-Type", "application/json")
225+
_ = json.NewEncoder(w).Encode(targets)
226+
}
227+
rDevtools.Get("/json", jsonTargetHandler)
228+
rDevtools.Get("/json/list", jsonTargetHandler)
229+
175230
rDevtools.Get("/*", func(w http.ResponseWriter, r *http.Request) {
176231
devtoolsproxy.WebSocketProxyHandler(upstreamMgr, slogger, config.LogCDPMessages, stz).ServeHTTP(w, r)
177232
})
@@ -227,3 +282,16 @@ func mustFFmpeg() {
227282
panic(fmt.Errorf("ffmpeg not found or not executable: %w", err))
228283
}
229284
}
285+
286+
// rewriteWSURL replaces the Chrome host with the proxy host in WebSocket URLs.
287+
// e.g., "ws://127.0.0.1:9223/devtools/page/..." -> "ws://127.0.0.1:9222/devtools/page/..."
288+
func rewriteWSURL(urlStr, chromeHost, proxyHost string) string {
289+
parsed, err := url.Parse(urlStr)
290+
if err != nil {
291+
return urlStr
292+
}
293+
if parsed.Host == chromeHost {
294+
parsed.Host = proxyHost
295+
}
296+
return parsed.String()
297+
}

0 commit comments

Comments
 (0)