Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 42 additions & 27 deletions adapters/fetchers/jshttp/jshttp.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package jshttp
import (
"context"
"errors"
"runtime"
"time"

"github.com/mxschmitt/playwright-go"
Expand Down Expand Up @@ -289,36 +290,50 @@ func (o *browser) Close() {
_ = o.browser.Close()
}

// launchArgs returns the Chromium command line flags for the given GOOS.
// --single-process is not passed on Windows, where current Chromium builds
// exit on the first navigation ("Target page, context or browser has been
// closed") when it is set.
func launchArgs(goos string, disableImages bool) []string {
args := []string{
`--start-maximized`,
`--no-default-browser-check`,
`--disable-dev-shm-usage`,
`--no-sandbox`,
`--disable-setuid-sandbox`,
`--no-zygote`,
`--disable-gpu`,
`--mute-audio`,
`--disable-extensions`,
`--disable-breakpad`,
`--disable-features=TranslateUI,BlinkGenPropertyTrees`,
`--disable-ipc-flooding-protection`,
`--enable-features=NetworkService,NetworkServiceInProcess`,
"--enable-features=NetworkService",
`--disable-default-apps`,
`--disable-notifications`,
`--disable-webgl`,
`--disable-blink-features=AutomationControlled`,
"--ignore-certificate-errors",
"--ignore-certificate-errors-spki-list",
"--disable-web-security",
}

if goos != "windows" {
args = append(args, `--single-process`)
}

if disableImages {
args = append(args, `--blink-settings=imagesEnabled=false`)
}

return args
}

func newBrowser(pw *playwright.Playwright, headless, disableImages bool, proxyPool *ProxyPool, ua string) (*browser, error) {
opts := playwright.BrowserTypeLaunchOptions{
Headless: playwright.Bool(headless),
Args: []string{
`--start-maximized`,
`--no-default-browser-check`,
`--disable-dev-shm-usage`,
`--no-sandbox`,
`--disable-setuid-sandbox`,
`--no-zygote`,
`--disable-gpu`,
`--mute-audio`,
`--disable-extensions`,
`--single-process`,
`--disable-breakpad`,
`--disable-features=TranslateUI,BlinkGenPropertyTrees`,
`--disable-ipc-flooding-protection`,
`--enable-features=NetworkService,NetworkServiceInProcess`,
"--enable-features=NetworkService",
`--disable-default-apps`,
`--disable-notifications`,
`--disable-webgl`,
`--disable-blink-features=AutomationControlled`,
"--ignore-certificate-errors",
"--ignore-certificate-errors-spki-list",
"--disable-web-security",
},
}
if disableImages {
opts.Args = append(opts.Args, `--blink-settings=imagesEnabled=false`)
Args: launchArgs(runtime.GOOS, disableImages),
}

br, err := pw.Chromium.Launch(opts)
Expand Down
38 changes: 38 additions & 0 deletions adapters/fetchers/jshttp/launch_args_internal_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package jshttp

import (
"slices"
"testing"
)

func TestLaunchArgsOmitSingleProcessOnWindows(t *testing.T) {
t.Parallel()

if slices.Contains(launchArgs("windows", false), "--single-process") {
t.Fatal("--single-process must not be passed on Windows: Chromium exits on the first navigation")
}
}

func TestLaunchArgsKeepSingleProcessOnOtherPlatforms(t *testing.T) {
t.Parallel()

for _, goos := range []string{"linux", "darwin"} {
if !slices.Contains(launchArgs(goos, false), "--single-process") {
t.Errorf("expected --single-process on %s", goos)
}
}
}

func TestLaunchArgsDisableImages(t *testing.T) {
t.Parallel()

const flag = "--blink-settings=imagesEnabled=false"

if slices.Contains(launchArgs("linux", false), flag) {
t.Errorf("did not expect %s when images are enabled", flag)
}

if !slices.Contains(launchArgs("linux", true), flag) {
t.Errorf("expected %s when images are disabled", flag)
}
}
4 changes: 3 additions & 1 deletion adapters/fetchers/jshttp/session_slot.go
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,9 @@ func (s *sessionSlot) acquirePage(ctx context.Context) (page, error) {

if err := s.runtime.recreatePage(); err != nil {
if err := s.runtime.recreateContext(); err != nil {
return nil, s.runtime.recreateBrowser()
if err := s.runtime.recreateBrowser(); err != nil {
return nil, err
}
}
}

Expand Down
54 changes: 54 additions & 0 deletions adapters/fetchers/jshttp/session_slot_internal_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package jshttp

import (
"context"
"errors"
"testing"
)

Expand Down Expand Up @@ -217,3 +218,56 @@ func TestSessionSlotRecreatesPageWhenPrimaryPageIsClosed(t *testing.T) {
t.Fatalf("expected closed page to be replaced")
}
}

// brokenPageRuntime cannot recreate pages or contexts, which forces
// acquirePage to fall back to recreating the whole browser.
type brokenPageRuntime struct {
*fakeRuntime
browserErr error
}

func (r *brokenPageRuntime) recreatePage() error { return errors.New("page closed") }

func (r *brokenPageRuntime) recreateContext() error { return errors.New("context closed") }

func (r *brokenPageRuntime) recreateBrowser() error {
if r.browserErr != nil {
return r.browserErr
}

return r.fakeRuntime.recreateBrowser()
}

func TestSessionSlotReturnsPageAfterBrowserRecreation(t *testing.T) {
t.Parallel()

slot := &sessionSlot{runtime: &brokenPageRuntime{fakeRuntime: &fakeRuntime{nextPageID: 1}}}

p, err := slot.acquirePage(context.Background())
if err != nil {
t.Fatalf("acquirePage returned error: %v", err)
}

if p == nil {
t.Fatalf("expected a page after browser recreation, got nil")
}
}

func TestSessionSlotReturnsErrorWhenBrowserRecreationFails(t *testing.T) {
t.Parallel()

errBrowser := errors.New("browser launch failed")
slot := &sessionSlot{runtime: &brokenPageRuntime{
fakeRuntime: &fakeRuntime{nextPageID: 1},
browserErr: errBrowser,
}}

p, err := slot.acquirePage(context.Background())
if !errors.Is(err, errBrowser) {
t.Fatalf("expected %v, got %v", errBrowser, err)
}

if p != nil {
t.Fatalf("expected no page when browser recreation fails, got %v", p)
}
}