diff --git a/pkg/modules/chromium/browser.go b/pkg/modules/chromium/browser.go index fd84915..471efcd 100644 --- a/pkg/modules/chromium/browser.go +++ b/pkg/modules/chromium/browser.go @@ -314,6 +314,14 @@ func (b *chromiumBrowser) do(ctx context.Context, logger *zap.Logger, url string listenForEventExceptionThrown(taskCtx, logger, &consoleExceptions, &consoleExceptionsMu) } + var ( + connectionRefused error + connectionRefusedMu sync.RWMutex + ) + + // See https://github.com/gotenberg/gotenberg/issues/913. + listenForEventLoadingFailedOnConnectionRefused(taskCtx, logger, &connectionRefused, &connectionRefusedMu) + err = chromedp.Run(taskCtx, tasks...) if err != nil { errMessage := err.Error() @@ -349,6 +357,14 @@ func (b *chromiumBrowser) do(ctx context.Context, logger *zap.Logger, url string return fmt.Errorf("%v: %w", consoleExceptions, ErrConsoleExceptions) } + // See https://github.com/gotenberg/gotenberg/issues/913. + connectionRefusedMu.RLock() + defer connectionRefusedMu.RUnlock() + + if connectionRefused != nil { + return fmt.Errorf("%v: %w", connectionRefused, ErrConnectionRefused) + } + return nil } diff --git a/pkg/modules/chromium/browser_test.go b/pkg/modules/chromium/browser_test.go index 84efcad..76764b7 100644 --- a/pkg/modules/chromium/browser_test.go +++ b/pkg/modules/chromium/browser_test.go @@ -249,6 +249,7 @@ func TestChromiumBrowser_pdf(t *testing.T) { browser browser fs *gotenberg.FileSystem options PdfOptions + url string noDeadline bool start bool expectError bool @@ -478,6 +479,32 @@ func TestChromiumBrowser_pdf(t *testing.T) { expectError: true, expectedError: ErrConsoleExceptions, }, + { + scenario: "ErrConnectionRefused", + browser: newChromiumBrowser( + browserArguments{ + binPath: os.Getenv("CHROMIUM_BIN_PATH"), + wsUrlReadTimeout: 5 * time.Second, + allowList: regexp2.MustCompile("", 0), + denyList: regexp2.MustCompile("", 0), + }, + ), + fs: func() *gotenberg.FileSystem { + fs := gotenberg.NewFileSystem() + + err := os.MkdirAll(fs.WorkingDirPath(), 0o755) + if err != nil { + t.Fatalf(fmt.Sprintf("expected no error but got: %v", err)) + } + + return fs + }(), + url: "http://localhost:100", + noDeadline: false, + start: true, + expectError: true, + expectedError: ErrConnectionRefused, + }, { scenario: "clear cache", browser: newChromiumBrowser( @@ -1243,10 +1270,15 @@ func TestChromiumBrowser_pdf(t *testing.T) { defer cancel() } + url := fmt.Sprintf("file://%s/index.html", tc.fs.WorkingDirPath()) + if tc.url != "" { + url = tc.url + } + err := tc.browser.pdf( ctx, logger, - fmt.Sprintf("file://%s/index.html", tc.fs.WorkingDirPath()), + url, fmt.Sprintf("%s/%s.pdf", tc.fs.WorkingDirPath(), uuid.NewString()), tc.options, ) @@ -1286,6 +1318,7 @@ func TestChromiumBrowser_screenshot(t *testing.T) { browser browser fs *gotenberg.FileSystem options ScreenshotOptions + url string noDeadline bool start bool expectError bool @@ -1519,6 +1552,33 @@ func TestChromiumBrowser_screenshot(t *testing.T) { expectError: true, expectedError: ErrConsoleExceptions, }, + { + scenario: "ErrConnectionRefused", + browser: newChromiumBrowser( + browserArguments{ + binPath: os.Getenv("CHROMIUM_BIN_PATH"), + wsUrlReadTimeout: 5 * time.Second, + allowList: regexp2.MustCompile("", 0), + denyList: regexp2.MustCompile("", 0), + }, + ), + + fs: func() *gotenberg.FileSystem { + fs := gotenberg.NewFileSystem() + + err := os.MkdirAll(fs.WorkingDirPath(), 0o755) + if err != nil { + t.Fatalf(fmt.Sprintf("expected no error but got: %v", err)) + } + + return fs + }(), + url: "http://localhost:100", + noDeadline: false, + start: true, + expectError: true, + expectedError: ErrConnectionRefused, + }, { scenario: "clear cache", browser: newChromiumBrowser( @@ -2169,10 +2229,15 @@ func TestChromiumBrowser_screenshot(t *testing.T) { defer cancel() } + url := fmt.Sprintf("file://%s/index.html", tc.fs.WorkingDirPath()) + if tc.url != "" { + url = tc.url + } + err := tc.browser.screenshot( ctx, logger, - fmt.Sprintf("file://%s/index.html", tc.fs.WorkingDirPath()), + url, fmt.Sprintf("%s/%s.pdf", tc.fs.WorkingDirPath(), uuid.NewString()), tc.options, ) diff --git a/pkg/modules/chromium/chromium.go b/pkg/modules/chromium/chromium.go index 3846c7b..8e3806e 100644 --- a/pkg/modules/chromium/chromium.go +++ b/pkg/modules/chromium/chromium.go @@ -42,6 +42,9 @@ var ( // is set to true. ErrConsoleExceptions = errors.New("console exceptions") + // ErrConnectionRefused happens when a URL cannot be reached. + ErrConnectionRefused = errors.New("connection refused") + // PDF specific. // ErrOmitBackgroundWithoutPrintBackground happens if diff --git a/pkg/modules/chromium/events.go b/pkg/modules/chromium/events.go index a329774..f43b2c8 100644 --- a/pkg/modules/chromium/events.go +++ b/pkg/modules/chromium/events.go @@ -64,8 +64,8 @@ func listenForEventRequestPaused(ctx context.Context, logger *zap.Logger, allowL }) } -// listenForEventResponseReceived listens for an invalid HTTP status code is -// returned by the main page. +// listenForEventResponseReceived listens for an invalid HTTP status code that +// is returned by the main page. // See https://github.com/gotenberg/gotenberg/issues/613. func listenForEventResponseReceived(ctx context.Context, logger *zap.Logger, url string, failOnHttpStatusCodes []int64, invalidHttpStatusCode *error, invalidHttpStatusCodeMu *sync.RWMutex) { for _, code := range []int64{199, 299, 399, 499, 599} { @@ -95,6 +95,28 @@ func listenForEventResponseReceived(ctx context.Context, logger *zap.Logger, url }) } +// listenForEventLoadingFailedOnConnectionRefused listens for an event +// indicating that the main page failed to load. +// See https://github.com/gotenberg/gotenberg/issues/913. +func listenForEventLoadingFailedOnConnectionRefused(ctx context.Context, logger *zap.Logger, connectionRefused *error, connectionRefusedMu *sync.RWMutex) { + chromedp.ListenTarget(ctx, func(ev interface{}) { + switch ev := ev.(type) { + case *network.EventLoadingFailed: + logger.Debug(fmt.Sprintf("event EventLoadingFailed fired: %+v", ev.ErrorText)) + + if ev.ErrorText != "net::ERR_CONNECTION_REFUSED" || ev.Type != network.ResourceTypeDocument { + logger.Debug("skip EventLoadingFailed: is not net::ERR_CONNECTION_REFUSED and/or resource type Document") + return + } + + connectionRefusedMu.Lock() + defer connectionRefusedMu.Unlock() + + *connectionRefused = fmt.Errorf("%s", ev.ErrorText) + } + }) +} + // listenForEventExceptionThrown listens for exceptions in the console and // appends those exceptions to the given error pointer. // See https://github.com/gotenberg/gotenberg/issues/262. diff --git a/pkg/modules/chromium/routes.go b/pkg/modules/chromium/routes.go index 3ee8d64..6edee14 100644 --- a/pkg/modules/chromium/routes.go +++ b/pkg/modules/chromium/routes.go @@ -688,5 +688,15 @@ func handleChromiumError(err error, options Options) error { ) } + if errors.Is(err, ErrConnectionRefused) { + return api.WrapError( + err, + api.NewSentinelHttpError( + http.StatusBadRequest, + "Chromium returned net::ERR_CONNECTION_REFUSED", + ), + ) + } + return err } diff --git a/pkg/modules/chromium/routes_test.go b/pkg/modules/chromium/routes_test.go index 050fa76..e09c161 100644 --- a/pkg/modules/chromium/routes_test.go +++ b/pkg/modules/chromium/routes_test.go @@ -1435,6 +1435,18 @@ func TestConvertUrl(t *testing.T) { expectHttpStatus: http.StatusConflict, expectOutputPathsCount: 0, }, + { + scenario: "ErrConnectionRefused", + ctx: &api.ContextMock{Context: new(api.Context)}, + api: &ApiMock{PdfMock: func(ctx context.Context, logger *zap.Logger, url, outputPath string, options PdfOptions) error { + return ErrConnectionRefused + }}, + options: DefaultPdfOptions(), + expectError: true, + expectHttpError: true, + expectHttpStatus: http.StatusBadRequest, + expectOutputPathsCount: 0, + }, { scenario: "error from Chromium", ctx: &api.ContextMock{Context: new(api.Context)}, @@ -1633,6 +1645,18 @@ func TestScreenshotUrl(t *testing.T) { expectHttpStatus: http.StatusConflict, expectOutputPathsCount: 0, }, + { + scenario: "ErrConnectionRefused", + ctx: &api.ContextMock{Context: new(api.Context)}, + api: &ApiMock{ScreenshotMock: func(ctx context.Context, logger *zap.Logger, url, outputPath string, options ScreenshotOptions) error { + return ErrConnectionRefused + }}, + options: DefaultScreenshotOptions(), + expectError: true, + expectHttpError: true, + expectHttpStatus: http.StatusBadRequest, + expectOutputPathsCount: 0, + }, { scenario: "error from Chromium", ctx: &api.ContextMock{Context: new(api.Context)},