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
9 changes: 5 additions & 4 deletions browser_context.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,15 @@ import (
"slices"
"strings"
"sync"
"sync/atomic"

"github.com/playwright-community/playwright-go/internal/safe"
)

type browserContextImpl struct {
channelOwner
timeoutSettings *timeoutSettings
closeWasCalled bool
closeWasCalled atomic.Bool
options *BrowserNewContextOptions
pages []Page
routes []*routeHandlerEntry
Expand Down Expand Up @@ -411,13 +412,13 @@ func (b *browserContextImpl) ExpectPage(cb func() error, options ...BrowserConte
}

func (b *browserContextImpl) Close(options ...BrowserContextCloseOptions) error {
if b.closeWasCalled {
if b.closeWasCalled.Load() {
return nil
}
if len(options) == 1 {
b.closeReason = options[0].Reason
}
b.closeWasCalled = true
b.closeWasCalled.Store(true)

_, err := b.channel.connection.WrapAPICall(func() (interface{}, error) {
return nil, b.request.Dispose(APIRequestContextDisposeOptions{
Expand Down Expand Up @@ -597,7 +598,7 @@ func (b *browserContextImpl) onRoute(route *routeImpl) {
url := route.Request().URL()
for _, handlerEntry := range routes {
// If the page or the context was closed we stall all requests right away.
if (page != nil && page.closeWasCalled) || b.closeWasCalled {
if (page != nil && page.closeWasCalled) || b.closeWasCalled.Load() {
return
}
if !handlerEntry.Matches(url) {
Expand Down
2 changes: 1 addition & 1 deletion page.go
Original file line number Diff line number Diff line change
Expand Up @@ -942,7 +942,7 @@ func (p *pageImpl) onRoute(route *routeImpl) {
url := route.Request().URL()
for _, handlerEntry := range routes {
// If the page was closed we stall all requests right away.
if p.closeWasCalled || p.browserContext.closeWasCalled {
if p.closeWasCalled || p.browserContext.closeWasCalled.Load() {
return
}
if !handlerEntry.Matches(url) {
Expand Down
108 changes: 108 additions & 0 deletions tests/browser_context_close_race_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
package playwright_test

import (
"os"
"testing"
"time"

"github.com/playwright-community/playwright-go"
"github.com/stretchr/testify/require"
)

// TestBrowserContextCloseRace tests for a data race between Close() and route handlers.
// This reproduces a race condition where Close() writes to closeWasCalled while
// route handler goroutines read it during page navigation.
//
// See: https://github.com/playwright-community/playwright-go/issues/XXX
func TestBrowserContextCloseRace(t *testing.T) {
// Create a minimal HAR file
harContent := `{
"log": {
"version": "1.2",
"creator": {"name": "test", "version": "1.0"},
"entries": [
{
"request": {
"method": "GET",
"url": "https://example.com/",
"httpVersion": "HTTP/2.0",
"headers": [],
"queryString": [],
"cookies": [],
"headersSize": -1,
"bodySize": 0
},
"response": {
"status": 200,
"statusText": "OK",
"httpVersion": "HTTP/2.0",
"headers": [{"name": "Content-Type", "value": "text/html"}],
"cookies": [],
"content": {
"size": 13,
"mimeType": "text/html",
"text": "Hello, World!"
},
"redirectURL": "",
"headersSize": -1,
"bodySize": 13
},
"cache": {},
"timings": {"send": 0, "wait": 0, "receive": 0}
}
]
}
}`

harFile, err := os.CreateTemp("", "test-*.har")
require.NoError(t, err)
defer os.Remove(harFile.Name())

_, err = harFile.WriteString(harContent)
require.NoError(t, err)
harFile.Close()

// Create a new context for this test (don't use BeforeEach)
testContext, err := browser.NewContext()
require.NoError(t, err)
defer testContext.Close()

// Set up HAR replay - registers internal route handlers
err = testContext.RouteFromHAR(harFile.Name(), playwright.BrowserContextRouteFromHAROptions{
NotFound: playwright.HarNotFoundAbort,
})
require.NoError(t, err)

// Add custom route handler
err = testContext.Route("**/version.json*", func(route playwright.Route) {
time.Sleep(5 * time.Millisecond) // Increase race window
_ = route.Fulfill(playwright.RouteFulfillOptions{
Status: playwright.Int(200),
ContentType: playwright.String("application/json"),
Body: playwright.String(`{"version": "1.0"}`),
})
})
require.NoError(t, err)

testPage, err := testContext.NewPage()
require.NoError(t, err)

// Start navigation in background
done := make(chan error, 1)
go func() {
_, err := testPage.Goto("https://example.com/")
done <- err
}()

// Give route handlers time to start processing
time.Sleep(20 * time.Millisecond)

// Close context while route handlers are actively running
// This triggers the race between Close() and the route handler goroutines
// Without proper synchronization, this will be detected by -race flag
err = testContext.Close()
require.NoError(t, err)

// Wait for navigation to complete
<-done
}
Loading