browser icon indicating copy to clipboard operation
browser copied to clipboard

can't connect to browser using go-rod/rod client

Open suntong opened this issue 3 weeks ago • 16 comments

Not specifically gomcp issue, but trying to reach Lightpanda Cloud Go support team for go client connection demo code instead.

We tried both chromedp and go-rod, both failed after exhausted trials. Here's only the Go-Rod trial parts.

Support Request: Connecting Go-Rod Client to Lightpanda WSS Endpoint

We are attempting to connect a Go application using the github.com/go-rod/rod library to the Lightpanda Cloud endpoint for browser automation. We are consistently encountering a 400 Bad Request panic during the initial WebSocket handshake, suggesting the authentication token is not being accepted in the expected format.

Environment & Goal

  • Client Library: github.com/go-rod/rod (Version assumed to be v0.116.2 or recent)

  • Target Endpoint: wss://euwest.cloud.lightpanda.io/ws

  • Authentication: Environment variable LPD_TOKEN is set and used.

  • Goal: Successfully connect rod.New().MustConnect() to the endpoint to begin automation.

Error Encountered

The panic is:

panic: websocket bad handshake: 400 Bad request. <html><body><h1>400 Bad request</h1> Your browser sent an invalid request.

</body></html>

This strongly indicates the server is rejecting the connection based on an invalid URL or missing/malformed authentication header.

Full final test code:

package main

import (
	"log"
	"os"
	"time"

	"github.com/go-rod/rod"
	"github.com/go-rod/rod/lib/launcher"
)

func main() {
	// --- 1. Connection Setup ---
	lpdToken := os.Getenv("LPD_TOKEN")
	if lpdToken == "" {
		log.Fatal("LPD_TOKEN environment variable is not set. Please set it.")
	}

	// Use the WSS URL with the token as the query parameter.
	wsURL := "wss://euwest.cloud.lightpanda.io/ws?token=" + lpdToken

	log.Printf("Attempting connection via ControlURL: %s", wsURL)

	// 2. Resolve the URL string using a standard launcher utility.
	// This function is often used to ensure the URL is correctly structured for rod's network client.
	u := launcher.MustResolveURL(wsURL)

	// 3. Create a rod browser instance using the resolved URL string.
	browser := rod.New().
		ControlURL(u). // Provide the resolved URL string
		Timeout(20 * time.Second).
		MustConnect()

	defer browser.MustClose()

	// --- 4. Automation Task ---
	page := browser.MustPage("").MustWaitLoad()
	defer page.MustClose()
	
	page.MustNavigate("https://example.com")

	title := page.MustInfo().Title
	
	// --- 5. Output the result ---
	log.Printf("Successfully connected to Lightpanda Cloud using rod.")
	log.Printf("Page Title: %s", title)
}

Exhaustive Connection Attempts Made (All Failed)

We have attempted every standard and low-level method provided by the go-rod library, but all resulted in the 400 Bad Request or an "undefined" function error due to package export issues in our installed version.

Attempt # Method Used Code Pattern Result Hypothesis/Reason for Failure
1 (Standard) Token via Query Param rod.New().ControlURL("wss://.../ws?token=%s").MustConnect() 400 Bad Request Server rejects token in query parameter format.
2 (Low-Level) External WebSocket (gorilla/websocket) Attempted to use websocket.Dial to inject Authorization: Bearer header, then pass to rod.New().Client(conn) rod.Client missing required Call method. Interface mismatch: *websocket.Conn does not implement rod.CDPClient.
3 (Public API) Core launcher utilities Attempted to use launcher.MustResolveURL or launcher.MustConnectRemote Undefined function (MustResolveURL, MustConnectRemote) Functions are not publicly exported in the installed go-rod version.
4 (Final Public API) Core rod functions Attempted to use rod.WebSocket(url).Connect() and client.Dial(url) Undefined function (rod.WebSocket, client.Dial) Functions are not publicly exported in the installed go-rod version.

Request for Assistance

Since the required low-level API functions in go-rod are not publicly available in our version, and the high-level method is rejected by your server, we require the exact, working Go code snippet for establishing the go-rod connection, specifically handling the authentication for the Lightpanda WSS endpoint.

Please confirm:

  1. Should the token be passed as a URL Query Parameter (e.g., ?token=...)?

  2. Should the token be passed as an HTTP Header (e.g., Authorization: Bearer <token>)?

  3. If a header is required, what is the exact key and format (e.g., Token: <token> or Authorization: Bearer <token>)?

Thank you for your help in resolving this handshake failure.

suntong avatar Nov 12 '25 09:11 suntong

Update,

Using the official "connect to an existing DevTools instance using a remote WebSocket URL" demo at remote, I'm getting

panic: websocket bad handshake: 400 Bad request. <html><body><h1>400 Bad request</h1>
Your browser sent an invalid request.
</body></html>

as well, here's more stack track info:

github.com/go-rod/rod/lib/utils.glob..func2({0x78d720?, 0xc000025b80?})
        /.../Go/pkg/mod/github.com/go-rod/[email protected]/lib/utils/utils.go:68 +0x1d
main.main.New.(*Browser).WithPanic.genE.func3({0xc0001f66c0?, 0x40ee65?, 0x10?})
        /.../Go/pkg/mod/github.com/go-rod/[email protected]/must.go:36 +0x5a
github.com/go-rod/rod.(*Browser).MustConnect(0xc00011e5a0)
        /.../Go/pkg/mod/github.com/go-rod/[email protected]/must.go:51 +0x7f

suntong avatar Nov 12 '25 12:11 suntong

Hey @suntong, thanks for the issue.

Regarding chromedp I'm really surprised you have issue, we use it internally without issue. We have a specific section in the doc to connect: https://lightpanda.io/docs/cloud-offer/tools/cdp#chromedp I would be pleased to have more details.

Regarding rod, I never tried it, but I will.

krichprollsch avatar Nov 12 '25 13:11 krichprollsch

Regarding chromedp I'm really surprised you have issue, we use it internally without issue.

Ah, sorry I forgot about that demo code, and I believe the chromedp.NoModifyURL did the tricks. thanks

As for rod, hope you can find sometime test it out as well, because I find it's more high level thus a bit easier to use than chromedp. I'll leave the issue open therefore. thx.

suntong avatar Nov 13 '25 02:11 suntong

Moving the issue to https://github.com/lightpanda-io/browser/ repository

krichprollsch avatar Nov 13 '25 07:11 krichprollsch

We have a specific section in the doc to connect: https://lightpanda.io/docs/cloud-offer/tools/cdp#chromedp

The code there doesn't compile, I don't know how to fix, and all my different hacky fixes got me into:

Failed getting title of lightpanda.io: invalid context

Appreciate having a code working out of the box.

Thanks Pierre.

suntong avatar Nov 13 '25 09:11 suntong

Oh indeed good catch, thanks. I fixed it here: https://gist.github.com/krichprollsch/0f5387493caa38b3a466c2ecbb094cc3

I will update doc.

krichprollsch avatar Nov 13 '25 11:11 krichprollsch

There is an incompatibility between our server's websocket gateway and rod's websocket client with the default Sec-WebSocket-Key value. See https://github.com/go-rod/rod/issues/1092#issuecomment-3528476306

I wrote a function to setup a connection with a valid Sec-WebSocket-Key header:

func NewBrowser(ctx context.Context, cdpws string) (*rod.Browser, func(), error) {
    // Generate a Sec-WebSocket-Key value.
    buf := make([]byte, 16)
    _, _ = rand.Read(buf)
    key := base64.StdEncoding.EncodeToString(buf)

    // Create a websocket and connect to the server.
    ws := &cdp.WebSocket{}
    err := ws.Connect(ctx, cdpws, http.Header{
        "Sec-WebSocket-Key": {key},
    })
    if err != nil {
        return nil, nil, err
    }

    cli := cdp.New()
    cli.Start(ws)

    b := rod.New()
    b.Trace(true)
    b.Client(cli)

    return b, func() {
        b.Close()
        ws.Close()
    }, nil
}

You can use with code like:

    b, cancel, err := NewBrowser(ctx, wsURL)
    if err != nil {
        return err
    }
    defer cancel()

    content := b.MustPage(url).MustWaitLoad().MustHTML()

But I still have an issue (with both Lightpanda and Chrome browser), I can't navigate to a page, rod just blocks. I try to understand why.

krichprollsch avatar Nov 13 '25 16:11 krichprollsch

I forgot the b.MustConnect() 🤦

    b, cancel, err := NewBrowser(ctx, wsURL)
    if err != nil {
        return err
    }
    defer cancel()

    content := b.MustConnect().MustPage(url).MustWaitLoad().MustHTML()

    fmt.Println(content)

Rod uses non implemented CDP command DOM.getOuterHTML.

 > {"id":24,"sessionId":"SID-1","method":"DOM.getOuterHTML","params":{"objectId":"5987356902031041503.2.8"}}
< {"id":24,"error":{"code":-31998,"message":"UnknownMethod"},"sessionId":"SID-1"}

krichprollsch avatar Nov 13 '25 16:11 krichprollsch

Rod uses non implemented CDP command DOM.getOuterHTML, causing UnknownMethod

I got the same error as well, and found it happens at MustHTML() call. I.e., MustWaitLoad() call was OK, but MustHTML() failed.

So, just to be clear, the unimplemented DOM.getOuterHTML was at lightpanda CDP side, right? (as Rod's MustHTML() works when not using lightpanda).

BTW, the page.MustInfo().Title got me into UnknownMethod as well.

suntong avatar Nov 14 '25 06:11 suntong

So, just to be clear, the unimplemented DOM.getOuterHTML was at lightpanda CDP side, right? (as Rod's MustHTML() works when not using lightpanda).

Yes it is, eveyr CDP client uses a different way to make the same action. I started the implementation in #1212 but I faced an issue I have to resolve.

BTW, the page.MustInfo().Title got me into UnknownMethod as well.

Thanks, I will take a look on it after.

In the meantime, you can test your script with chrome in our cloud by adding &browser=chrome in the ws url. See https://lightpanda.io/docs/cloud-offer/tools/cdp#browser for details

krichprollsch avatar Nov 14 '25 07:11 krichprollsch

you can test your script with chrome in our cloud by adding &browser=chrome in the ws url

Thanks!!! That works like a charm!

Both MustHTML() and page.MustInfo().Title are working now. Feel free to close this issue now, or after page.MustInfo().Title is fixed.

BTW, Really appreciate that you make it work for me, as

I find it (rod) is more high level thus a bit easier to use than chromedp.

And here's a concrete example to justify that ...

UPDATE, it's actually a different issue, reported in a new thread in https://github.com/lightpanda-io/browser/issues/1214.

suntong avatar Nov 14 '25 09:11 suntong

I fixed both HTML() (#1216) and MustInfo().Title (#1218) For Lightpanda. I also added example usage + tests for Rod in our demo repository: https://github.com/lightpanda-io/demo/tree/main/rod

krichprollsch avatar Nov 19 '25 08:11 krichprollsch

I fixed both HTML() (#1216) and MustInfo().Title (#1218) For Lightpanda. I also added example usage + tests for Rod in our demo repository: https://github.com/lightpanda-io/demo/tree/main/rod

Thanks for the fix and demo. I tried it at my end, but didn't get the page title:

export CDPCLI_WS='wss://euwest.cloud.lightpanda.io/ws?token=d12...

$ go run title.go -verbose https://example.com/
[rod] 2025/11/22 21:19:37 [wait] load <page:TID-1>

The title.go is copied from https://github.com/lightpanda-io/demo/blob/main/rod/title/main.go as-is.

Testing with my OP sample code, I got into panic UnknownMethod again.

Here's the full code:

package main

import (
	"context"
	"crypto/rand"
	"encoding/base64"
	"log"
	"net/http"
	"os"

	"github.com/go-rod/rod"
	"github.com/go-rod/rod/lib/cdp"
)

func main() {
	// --- Connection Setup ---
	lpdToken := os.Getenv("LPD_TOKEN")
	if lpdToken == "" {
		log.Fatal("LPD_TOKEN environment variable is not set. Please set it.")
	}

	// Use the WSS URL with the token as the query parameter.
	wsURL := "wss://euwest.cloud.lightpanda.io/ws?token=" + lpdToken

	log.Printf("Attempting connection via ControlURL: %s", wsURL)

	b, cancel, err := NewBrowser(context.Background(), wsURL)
	if err != nil {
		return
	}
	defer cancel()

	// 3. Create a browser instance
	browser := b.MustConnect()
	defer browser.MustClose()

	// --- 4. Automation Task ---
	url := "https://example.com"
	page := browser.MustPage(url)
	defer page.MustClose()
        page.MustWaitLoad()

	//page.MustNavigate("https://example.com")
	log.Printf("Successfully connected to Lightpanda Cloud using rod.")

	title := page.MustInfo().Title
	content := page.MustHTML()

	// --- 5. Output the result ---
	log.Printf("Page Title: %s", title)
	log.Printf("HTML %s", content)
}

func NewBrowser(ctx context.Context, cdpws string) (*rod.Browser, func(), error) {
	// Generate a Sec-WebSocket-Key value.
	buf := make([]byte, 16)
	_, _ = rand.Read(buf)
	key := base64.StdEncoding.EncodeToString(buf)

	// Create a websocket and connect to the server.
	ws := &cdp.WebSocket{}
	err := ws.Connect(ctx, cdpws, http.Header{
		"Sec-WebSocket-Key": {key},
	})
	if err != nil {
		return nil, nil, err
	}

	cli := cdp.New()
	cli.Start(ws)

	b := rod.New()
	b.Trace(true)
	b.Client(cli)

	return b, func() {
		b.Close()
		ws.Close()
	}, nil
}

which works fine after adding browser=chrome:

@@ -22,3 +22,3 @@
        // Use the WSS URL with the token as the query parameter.
-       wsURL := "wss://euwest.cloud.lightpanda.io/ws?token=" + lpdToken
+       wsURL := "wss://euwest.cloud.lightpanda.io/ws?browser=chrome&token=" + lpdToken

Please double-check. thx.

suntong avatar Nov 23 '25 02:11 suntong

BTW, I tried go get -u github.com/go-rod/rod before running the demo again, but

  1. I noticed that the go-rod/rod remains at v0.116.2, yet
  2. some dependents got updated, and when I running the demo again, I'm getting:
...github.com/go-rod/[email protected]/lib/launcher/browser.go:138:25: too many arguments in call to fetchup.New
        have (string, []string)
        want (...string)

Please double-check as well. thx.

suntong avatar Nov 23 '25 02:11 suntong

@suntong The Lightpanda borwser on the cloud wasn't updated yet. I forced an update which is in deployment on rolling basis right now. So the issue will be fix.

I will check the rod update

krichprollsch avatar Nov 24 '25 08:11 krichprollsch

I will check the rod update

Seems to be necessary as I just tested with my OP sample code again and I got into panic UnknownMethod again.

suntong avatar Nov 30 '25 07:11 suntong