Skip to content

feat: add ConnectRemote to drive a remote Playwright server without a local driver - #644

Open
minhhuynh-tinyfish wants to merge 1 commit into
mxschmitt:mainfrom
minhhuynh-tinyfish:feat/connect-remote-websocket
Open

minhhuynh-tinyfish wants to merge 1 commit into
mxschmitt:mainfrom
minhhuynh-tinyfish:feat/connect-remote-websocket

Conversation

@minhhuynh-tinyfish

Copy link
Copy Markdown

This PR adds playwright.ConnectRemote(ctx, wsEndpoint, opts). It connects to a BrowserType.launchServer() endpoint directly over WebSocket, so the client machine doesn't need a Node.js driver.

Why

BrowserType.Connect still needs playwright.Run(). The local driver opens the WebSocket (LocalUtils.connect), and every message goes through it as a JsonPipe. If you run many Go clients against a remote browser fleet, that means one Node.js process per client. It adds no capability, but it costs memory, CPU and image size, and the driver has to be installed on every client host.

How

The Go client already speaks the raw protocol to the driver's dispatcher. A launchServer endpoint is the same dispatcher reached over a WebSocket, so the only wire difference is framing: each message is one JSON text frame instead of a length-prefixed stdio message.

  • ws_transport.go: a transport implementation on github.com/coder/websocket, which is already in go.mod (the tests use it). Compression is off. The read limit is raised to 256 MiB to match the server's own limit, since file payloads travel base64-encoded inside a single message.
  • connect_remote.go: runs connection.Start() over that transport and returns the preLaunchedBrowser, wired up the same way BrowserType.Connect does. The connection is marked remote, so uploads send file contents (payloads/streams) and artifacts download through saveAsStream. Neither touches the server's filesystem.

Version compatibility

The server compares the major.minor version in a Playwright/x.y.z User-Agent with its own, and refuses a mismatch with HTTP 428. If it can't parse the User-Agent, it skips the check entirely. So ConnectRemote handles the header like this:

  • It always sends Playwright/<playwrightCliVersion> (<arch>; <os>) go/<version>, so the header follows each roll automatically.
  • It ignores a User-Agent supplied through Headers. Otherwise a caller could silently turn the check off and hit confusing protocol errors later.
  • It returns a 428 as the new ErrVersionMismatch, naming both versions and the release to use. For example: the server at ws://… runs Playwright 1.63 but this client speaks Playwright 1.62; use the playwright-go release built for Playwright 1.63 (tag v0.63xx.x).

Running without LocalUtils

A remote server doesn't provide LocalUtils (utils is omitted when isServer). tracing.go and connection.go now skip client-side stack collection when it is missing instead of dereferencing nil. In the existing local and BrowserType.Connect modes localUtils is always set, so nothing changes there.

Known limitations, also listed in the godoc:

  • Devices is empty.
  • RouteFromHAR and recording HAR to a non-zip file are unsupported. They currently hit a nil LocalUtils; I'd follow up with a clear error.
  • Traces don't include client-side call stacks or sources. The server's trace archive is still valid.

Testing

  • Unit tests in connect_remote_test.go. They check that the User-Agent passes the server's own regex and major.minor comparison, that caller headers are forwarded but User-Agent can't be overridden, and that a 428 maps to ErrVersionMismatch. The 428 body was captured from a real server. They also check that other handshake failures stay distinct.
  • A manual end-to-end run against firefox.launchServer() from playwright-core 1.62.1, reached through a pass-through WebSocket relay:
    • connect (no child processes on the Go side), new context and page, Goto, Evaluate, screenshot
    • SetInputFiles from a path and from a buffer, a download plus SaveAs
    • Route plus Fulfill, tracing start and stop to a valid zip
    • Browser.Close(), after which further calls report target closed
  • Servers started with PW_VERSION_OVERRIDE: 1.63.0 and 1.61.5 were refused with ErrVersionMismatch, and 1.62.0 connected.
  • -race is clean, golangci-lint reports 0 issues, and the existing root-package tests pass.

Not included yet: an integration test under tests/ that starts a launchServer and runs ConnectRemote in CI (remote_server_test.go already starts such a server). I'm happy to add it to this PR, or to change the API shape if you'd prefer something else.

🤖 Generated with Claude Code

… local driver

ConnectRemote speaks the Playwright protocol directly to a
BrowserType.launchServer endpoint over WebSocket (one JSON text frame per
message), so no Node.js driver is downloaded or started on the client.
BrowserType.Connect still needs one, because it opens the WebSocket
through LocalUtils.connect on the local driver.

The handshake always sends a Playwright/<driver version> User-Agent and
does not let callers override it: the server only enforces its
major.minor compatibility check for a User-Agent it can parse. A 428
refusal is returned as ErrVersionMismatch naming both versions.

Without a local driver there is no LocalUtils, so tracing skips
client-side stack collection instead of dereferencing nil.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

This branch has not been deployed

No deployments
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.

1 participant