fix(chromium): register lifecycle listeners before navigate to close race

This commit is contained in:
Julien Neuhart
2026-05-22 09:39:25 +02:00
parent 7f9c3e171c
commit 2a9bf6bf11
2 changed files with 76 additions and 60 deletions

View File

@@ -428,10 +428,15 @@ func listenForEventExceptionThrown(ctx context.Context, logger *slog.Logger, con
})
}
// waitForEventDomContentEventFired waits until the event DomContentEventFired
// is fired or the context timeout.
// waitForEventDomContentEventFired registers a listener for the
// DomContentEventFired event and returns a waiter that blocks until the
// event fires or ctx is done. The listener registers at call time, not
// inside the waiter, so callers must invoke this before triggering the
// action that may emit the event. Registering inside the waiter would
// open a race: for fast loads (typically file:// pages with no external
// sub-resources), the event can fire before the waiter goroutine starts
// and the listener never sees a record.
func waitForEventDomContentEventFired(ctx context.Context, logger *slog.Logger) func() error {
return func() error {
ch := make(chan struct{})
cctx, cancel := context.WithCancel(ctx)
chromedp.ListenTarget(cctx, func(ev any) {
@@ -440,7 +445,7 @@ func waitForEventDomContentEventFired(ctx context.Context, logger *slog.Logger)
close(ch)
}
})
return func() error {
select {
case <-ch:
logger.DebugContext(ctx, "event DomContentEventFired fired")
@@ -451,10 +456,11 @@ func waitForEventDomContentEventFired(ctx context.Context, logger *slog.Logger)
}
}
// waitForEventLoadEventFired waits until the event LoadEventFired is fired or
// the context timeout.
// waitForEventLoadEventFired registers a listener for the LoadEventFired
// event and returns a waiter that blocks until the event fires or ctx is
// done. See [waitForEventDomContentEventFired] for the rationale on
// registering at call time rather than inside the waiter.
func waitForEventLoadEventFired(ctx context.Context, logger *slog.Logger) func() error {
return func() error {
ch := make(chan struct{})
cctx, cancel := context.WithCancel(ctx)
chromedp.ListenTarget(cctx, func(ev any) {
@@ -463,7 +469,7 @@ func waitForEventLoadEventFired(ctx context.Context, logger *slog.Logger) func()
close(ch)
}
})
return func() error {
select {
case <-ch:
logger.DebugContext(ctx, "event LoadEventFired fired")
@@ -474,10 +480,11 @@ func waitForEventLoadEventFired(ctx context.Context, logger *slog.Logger) func()
}
}
// waitForEventNetworkIdle waits until the event networkIdle is fired or the
// context timeout.
// waitForEventNetworkIdle registers a listener for the networkIdle
// lifecycle event and returns a waiter that blocks until the event fires
// or ctx is done. See [waitForEventDomContentEventFired] for the
// rationale on registering at call time rather than inside the waiter.
func waitForEventNetworkIdle(ctx context.Context, logger *slog.Logger) func() error {
return func() error {
ch := make(chan struct{})
cctx, cancel := context.WithCancel(ctx)
chromedp.ListenTarget(cctx, func(ev any) {
@@ -486,7 +493,7 @@ func waitForEventNetworkIdle(ctx context.Context, logger *slog.Logger) func() er
close(ch)
}
})
return func() error {
select {
case <-ch:
logger.DebugContext(ctx, "event networkIdle fired")
@@ -497,10 +504,11 @@ func waitForEventNetworkIdle(ctx context.Context, logger *slog.Logger) func() er
}
}
// waitForEventNetworkAlmostIdle waits until the event networkIdle2 is fired
// or the context timeout.
// waitForEventNetworkAlmostIdle registers a listener for the networkIdle2
// lifecycle event and returns a waiter that blocks until the event fires
// or ctx is done. See [waitForEventDomContentEventFired] for the
// rationale on registering at call time rather than inside the waiter.
func waitForEventNetworkAlmostIdle(ctx context.Context, logger *slog.Logger) func() error {
return func() error {
ch := make(chan struct{})
cctx, cancel := context.WithCancel(ctx)
chromedp.ListenTarget(cctx, func(ev any) {
@@ -509,7 +517,7 @@ func waitForEventNetworkAlmostIdle(ctx context.Context, logger *slog.Logger) fun
close(ch)
}
})
return func() error {
select {
case <-ch:
logger.DebugContext(ctx, "event networkAlmostIdle fired")
@@ -520,10 +528,11 @@ func waitForEventNetworkAlmostIdle(ctx context.Context, logger *slog.Logger) fun
}
}
// waitForEventLoadingFinished waits until the event LoadingFinished is fired
// or the context timeout.
// waitForEventLoadingFinished registers a listener for the
// LoadingFinished event and returns a waiter that blocks until the event
// fires or ctx is done. See [waitForEventDomContentEventFired] for the
// rationale on registering at call time rather than inside the waiter.
func waitForEventLoadingFinished(ctx context.Context, logger *slog.Logger) func() error {
return func() error {
ch := make(chan struct{})
cctx, cancel := context.WithCancel(ctx)
chromedp.ListenTarget(cctx, func(ev any) {
@@ -532,7 +541,7 @@ func waitForEventLoadingFinished(ctx context.Context, logger *slog.Logger) func(
close(ch)
}
})
return func() error {
select {
case <-ch:
logger.DebugContext(ctx, "event LoadingFinished fired")

View File

@@ -332,11 +332,13 @@ func navigateActionFunc(logger *slog.Logger, url string, skipNetworkIdleEvent, s
return func(ctx context.Context) error {
logger.DebugContext(ctx, fmt.Sprintf("navigate to '%s'", url))
_, _, _, _, err := page.Navigate(url).Do(ctx)
if err != nil {
return fmt.Errorf("navigate to '%s': %w", url, err)
}
// Register lifecycle listeners before issuing Page.navigate. For
// fast loads (typically file:// pages with no external
// sub-resources), DomContentEventFired / LoadEventFired /
// LoadingFinished can fire between Navigate.Do returning and
// runBatch spawning the waiter goroutines. Registering ahead of
// the navigate command closes that race.
// See https://github.com/gotenberg/gotenberg/issues/1561.
waitFunc := []func() error{
waitForEventDomContentEventFired(ctx, logger),
waitForEventLoadEventFired(ctx, logger),
@@ -355,6 +357,11 @@ func navigateActionFunc(logger *slog.Logger, url string, skipNetworkIdleEvent, s
logger.DebugContext(ctx, "skipping network almost idle event")
}
_, _, _, _, err := page.Navigate(url).Do(ctx)
if err != nil {
return fmt.Errorf("navigate to '%s': %w", url, err)
}
err = runBatch(
ctx,
waitFunc...,