diff --git a/pkg/modules/chromium/browser.go b/pkg/modules/chromium/browser.go index 4e89520..380dccc 100644 --- a/pkg/modules/chromium/browser.go +++ b/pkg/modules/chromium/browser.go @@ -299,13 +299,25 @@ func (b *chromiumBrowser) do(ctx context.Context, logger *zap.Logger, url string }) var ( - invalidHttpStatusCode error - invalidHttpStatusCodeMu sync.RWMutex + invalidHttpStatusCode error + invalidHttpStatusCodeMu sync.RWMutex + invalidResourceHttpStatusCode error + invalidResourceHttpStatusCodeMu sync.RWMutex ) - // See https://github.com/gotenberg/gotenberg/issues/613. - if len(options.FailOnHttpStatusCodes) != 0 { - listenForEventResponseReceived(taskCtx, logger, url, options.FailOnHttpStatusCodes, &invalidHttpStatusCode, &invalidHttpStatusCodeMu) + // See: + // https://github.com/gotenberg/gotenberg/issues/613. + // https://github.com/gotenberg/gotenberg/issues/1021. + if len(options.FailOnHttpStatusCodes) != 0 || len(options.FailOnResourceHttpStatusCodes) != 0 { + listenForEventResponseReceived(taskCtx, logger, eventResponseReceivedOptions{ + mainPageUrl: url, + failOnHttpStatusCodes: options.FailOnHttpStatusCodes, + invalidHttpStatusCode: &invalidHttpStatusCode, + invalidHttpStatusCodeMu: &invalidHttpStatusCodeMu, + failOnResourceOnHttpStatusCode: options.FailOnResourceHttpStatusCodes, + invalidResourceHttpStatusCode: &invalidResourceHttpStatusCode, + invalidResourceHttpStatusCodeMu: &invalidResourceHttpStatusCodeMu, + }) } var ( @@ -319,14 +331,22 @@ func (b *chromiumBrowser) do(ctx context.Context, logger *zap.Logger, url string } var ( - loadingFailed error - loadingFailedMu sync.RWMutex + loadingFailed error + loadingFailedMu sync.RWMutex + resourceLoadingFailed error + resourceLoadingFailedMu sync.RWMutex ) // See: - // https://github.com/gotenberg/gotenberg/issues/913 - // https://github.com/gotenberg/gotenberg/issues/959 - listenForEventLoadingFailed(taskCtx, logger, &loadingFailed, &loadingFailedMu) + // https://github.com/gotenberg/gotenberg/issues/913. + // https://github.com/gotenberg/gotenberg/issues/959. + // https://github.com/gotenberg/gotenberg/issues/1021. + listenForEventLoadingFailed(taskCtx, logger, eventLoadingFailedOptions{ + loadingFailed: &loadingFailed, + loadingFailedMu: &loadingFailedMu, + resourceLoadingFailed: &resourceLoadingFailed, + resourceLoadingFailedMu: &resourceLoadingFailedMu, + }) err = chromedp.Run(taskCtx, tasks...) if err != nil { @@ -355,6 +375,14 @@ func (b *chromiumBrowser) do(ctx context.Context, logger *zap.Logger, url string return fmt.Errorf("%v: %w", invalidHttpStatusCode, ErrInvalidHttpStatusCode) } + // See https://github.com/gotenberg/gotenberg/issues/1021. + invalidResourceHttpStatusCodeMu.RLock() + defer invalidResourceHttpStatusCodeMu.RUnlock() + + if invalidResourceHttpStatusCode != nil { + return fmt.Errorf("%v: %w", invalidResourceHttpStatusCode, ErrInvalidResourceHttpStatusCode) + } + // See https://github.com/gotenberg/gotenberg/issues/262. consoleExceptionsMu.RLock() defer consoleExceptionsMu.RUnlock() @@ -364,8 +392,8 @@ func (b *chromiumBrowser) do(ctx context.Context, logger *zap.Logger, url string } // See: - // https://github.com/gotenberg/gotenberg/issues/913 - // https://github.com/gotenberg/gotenberg/issues/959 + // https://github.com/gotenberg/gotenberg/issues/913. + // https://github.com/gotenberg/gotenberg/issues/959. loadingFailedMu.RLock() defer loadingFailedMu.RUnlock() @@ -373,6 +401,13 @@ func (b *chromiumBrowser) do(ctx context.Context, logger *zap.Logger, url string return fmt.Errorf("%v: %w", loadingFailed, ErrLoadingFailed) } + // See https://github.com/gotenberg/gotenberg/issues/1021. + if options.FailOnResourceLoadingFailed { + if resourceLoadingFailed != nil { + return fmt.Errorf("%v: %w", resourceLoadingFailed, ErrResourceLoadingFailed) + } + } + return nil } diff --git a/pkg/modules/chromium/browser_test.go b/pkg/modules/chromium/browser_test.go index 9cc4bcb..3ba608b 100644 --- a/pkg/modules/chromium/browser_test.go +++ b/pkg/modules/chromium/browser_test.go @@ -446,6 +446,44 @@ func TestChromiumBrowser_pdf(t *testing.T) { expectError: true, expectedError: ErrInvalidHttpStatusCode, }, + { + scenario: "ErrInvalidResourceHttpStatusCode", + 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)) + } + + err = os.WriteFile(fmt.Sprintf("%s/style.css", fs.WorkingDirPath()), []byte("body{font-family: Arial, Helvetica, sans-serif;}"), 0o755) + if err != nil { + t.Fatalf("expected no error but got: %v", err) + } + + err = os.WriteFile(fmt.Sprintf("%s/index.html", fs.WorkingDirPath()), []byte("

ErrInvalidResourceHttpStatusCode

"), 0o755) + if err != nil { + t.Fatalf("expected no error but got: %v", err) + } + + return fs + }(), + options: PdfOptions{ + Options: Options{FailOnResourceHttpStatusCodes: []int64{200}}, + }, + noDeadline: false, + start: true, + expectError: true, + expectedError: ErrInvalidResourceHttpStatusCode, + }, { scenario: "ErrConsoleExceptions", browser: newChromiumBrowser( @@ -505,6 +543,39 @@ func TestChromiumBrowser_pdf(t *testing.T) { expectError: true, expectedError: ErrLoadingFailed, }, + { + scenario: "ErrResourceLoadingFailed", + 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)) + } + + err = os.WriteFile(fmt.Sprintf("%s/index.html", fs.WorkingDirPath()), []byte("

ErrResourceLoadingFailed

"), 0o755) + if err != nil { + t.Fatalf("expected no error but got: %v", err) + } + + return fs + }(), + options: PdfOptions{ + Options: Options{FailOnResourceLoadingFailed: true}, + }, + noDeadline: false, + start: true, + expectError: true, + expectedError: ErrResourceLoadingFailed, + }, { scenario: "clear cache", browser: newChromiumBrowser( @@ -1537,6 +1608,44 @@ func TestChromiumBrowser_screenshot(t *testing.T) { expectError: true, expectedError: ErrInvalidHttpStatusCode, }, + { + scenario: "ErrInvalidResourceHttpStatusCode", + 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)) + } + + err = os.WriteFile(fmt.Sprintf("%s/style.css", fs.WorkingDirPath()), []byte("body{font-family: Arial, Helvetica, sans-serif;}"), 0o755) + if err != nil { + t.Fatalf("expected no error but got: %v", err) + } + + err = os.WriteFile(fmt.Sprintf("%s/index.html", fs.WorkingDirPath()), []byte("

ErrInvalidResourceHttpStatusCode

"), 0o755) + if err != nil { + t.Fatalf("expected no error but got: %v", err) + } + + return fs + }(), + options: ScreenshotOptions{ + Options: Options{FailOnResourceHttpStatusCodes: []int64{299}}, + }, + noDeadline: false, + start: true, + expectError: true, + expectedError: ErrInvalidResourceHttpStatusCode, + }, { scenario: "ErrConsoleExceptions", browser: newChromiumBrowser( @@ -1580,7 +1689,6 @@ func TestChromiumBrowser_screenshot(t *testing.T) { denyList: regexp2.MustCompile("", 0), }, ), - fs: func() *gotenberg.FileSystem { fs := gotenberg.NewFileSystem() @@ -1597,6 +1705,39 @@ func TestChromiumBrowser_screenshot(t *testing.T) { expectError: true, expectedError: ErrLoadingFailed, }, + { + scenario: "ErrResourceLoadingFailed", + 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)) + } + + err = os.WriteFile(fmt.Sprintf("%s/index.html", fs.WorkingDirPath()), []byte("

ErrResourceLoadingFailed

"), 0o755) + if err != nil { + t.Fatalf("expected no error but got: %v", err) + } + + return fs + }(), + options: ScreenshotOptions{ + Options: Options{FailOnResourceLoadingFailed: true}, + }, + noDeadline: false, + start: true, + expectError: true, + expectedError: ErrResourceLoadingFailed, + }, { scenario: "clear cache", browser: newChromiumBrowser( diff --git a/pkg/modules/chromium/chromium.go b/pkg/modules/chromium/chromium.go index 0a8a8a2..08f3c64 100644 --- a/pkg/modules/chromium/chromium.go +++ b/pkg/modules/chromium/chromium.go @@ -38,14 +38,22 @@ var ( // matches with one of the entry in [Options.FailOnHttpStatusCodes]. ErrInvalidHttpStatusCode = errors.New("invalid HTTP status code") + // ErrInvalidResourceHttpStatusCode happens when the status code from one + // or more resources matches with one of the entry in + // [Options.FailOnResourceHttpStatusCodes]. + ErrInvalidResourceHttpStatusCode = errors.New("invalid resource HTTP status code") + // ErrConsoleExceptions happens when there are exceptions in the Chromium // console. It also happens only if the [Options.FailOnConsoleExceptions] // is set to true. ErrConsoleExceptions = errors.New("console exceptions") - // ErrLoadingFailed happens when a URL failed to load. + // ErrLoadingFailed happens when the main page failed to load. ErrLoadingFailed = errors.New("loading failed") + // ErrResourceLoadingFailed happens when one or more resources failed to load. + ErrResourceLoadingFailed = errors.New("resource loading failed") + // PDF specific. // ErrOmitBackgroundWithoutPrintBackground happens if @@ -86,6 +94,14 @@ type Options struct { // code from the main page matches with one of its entries. FailOnHttpStatusCodes []int64 + // FailOnResourceHttpStatusCodes sets if the conversion should fail if the + // status code from at least one resource matches with one if its entries. + FailOnResourceHttpStatusCodes []int64 + + // FailOnResourceLoadingFailed sets if the conversion should fail like the + // main page if Chromium fails to load at least one resource. + FailOnResourceLoadingFailed bool + // FailOnConsoleExceptions sets if the conversion should fail if there are // exceptions in the Chromium console. FailOnConsoleExceptions bool @@ -124,17 +140,19 @@ type Options struct { // DefaultOptions returns the default values for Options. func DefaultOptions() Options { return Options{ - SkipNetworkIdleEvent: true, - FailOnHttpStatusCodes: []int64{499, 599}, - FailOnConsoleExceptions: false, - WaitDelay: 0, - WaitWindowStatus: "", - WaitForExpression: "", - Cookies: nil, - UserAgent: "", - ExtraHttpHeaders: nil, - EmulatedMediaType: "", - OmitBackground: false, + SkipNetworkIdleEvent: true, + FailOnHttpStatusCodes: []int64{499, 599}, + FailOnResourceHttpStatusCodes: nil, + FailOnResourceLoadingFailed: false, + FailOnConsoleExceptions: false, + WaitDelay: 0, + WaitWindowStatus: "", + WaitForExpression: "", + Cookies: nil, + UserAgent: "", + ExtraHttpHeaders: nil, + EmulatedMediaType: "", + OmitBackground: false, } } diff --git a/pkg/modules/chromium/events.go b/pkg/modules/chromium/events.go index 85d7145..ec02ef0 100644 --- a/pkg/modules/chromium/events.go +++ b/pkg/modules/chromium/events.go @@ -3,6 +3,7 @@ package chromium import ( "context" "fmt" + "net/http" "slices" "sync" @@ -136,14 +137,36 @@ func listenForEventRequestPaused(ctx context.Context, logger *zap.Logger, option }) } +type eventResponseReceivedOptions struct { + mainPageUrl string + failOnHttpStatusCodes []int64 + invalidHttpStatusCode *error + invalidHttpStatusCodeMu *sync.RWMutex + failOnResourceOnHttpStatusCode []int64 + invalidResourceHttpStatusCode *error + invalidResourceHttpStatusCodeMu *sync.RWMutex +} + // 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) { +// is returned by the main page or by one or more resources. +// See: +// https://github.com/gotenberg/gotenberg/issues/613. +// https://github.com/gotenberg/gotenberg/issues/1021. +func listenForEventResponseReceived( + ctx context.Context, + logger *zap.Logger, + options eventResponseReceivedOptions, +) { for _, code := range []int64{199, 299, 399, 499, 599} { - if slices.Contains(failOnHttpStatusCodes, code) { + if slices.Contains(options.failOnHttpStatusCodes, code) { for i := code - 99; i <= code; i++ { - failOnHttpStatusCodes = append(failOnHttpStatusCodes, i) + options.failOnHttpStatusCodes = append(options.failOnHttpStatusCodes, i) + } + } + + if slices.Contains(options.failOnResourceOnHttpStatusCode, code) { + for i := code - 99; i <= code; i++ { + options.failOnResourceOnHttpStatusCode = append(options.failOnResourceOnHttpStatusCode, i) } } } @@ -151,41 +174,53 @@ func listenForEventResponseReceived(ctx context.Context, logger *zap.Logger, url chromedp.ListenTarget(ctx, func(ev interface{}) { switch ev := ev.(type) { case *network.EventResponseReceived: - if ev.Response.URL != url { + if ev.Response.URL == options.mainPageUrl { + logger.Debug(fmt.Sprintf("event EventResponseReceived fired for main page: %+v", ev.Response)) + + if slices.Contains(options.failOnHttpStatusCodes, ev.Response.Status) { + options.invalidHttpStatusCodeMu.Lock() + defer options.invalidHttpStatusCodeMu.Unlock() + + *options.invalidHttpStatusCode = fmt.Errorf("%d: %s", ev.Response.Status, ev.Response.StatusText) + } + return } - logger.Debug(fmt.Sprintf("event EventResponseReceived fired for main page: %+v", ev.Response)) + logger.Debug(fmt.Sprintf("event EventResponseReceived fired for a resource: %+v", ev.Response)) - if slices.Contains(failOnHttpStatusCodes, ev.Response.Status) { - invalidHttpStatusCodeMu.Lock() - defer invalidHttpStatusCodeMu.Unlock() + if slices.Contains(options.failOnResourceOnHttpStatusCode, ev.Response.Status) { + options.invalidResourceHttpStatusCodeMu.Lock() + defer options.invalidResourceHttpStatusCodeMu.Unlock() - *invalidHttpStatusCode = fmt.Errorf("%d: %s", ev.Response.Status, ev.Response.StatusText) + *options.invalidResourceHttpStatusCode = multierr.Append( + *options.invalidResourceHttpStatusCode, + fmt.Errorf("%s - %d: %s", ev.Response.URL, ev.Response.Status, http.StatusText(int(ev.Response.Status))), + ) } } }) } +type eventLoadingFailedOptions struct { + loadingFailed *error + loadingFailedMu *sync.RWMutex + resourceLoadingFailed *error + resourceLoadingFailedMu *sync.RWMutex +} + // listenForEventLoadingFailed listens for an event indicating that the main -// page failed to load. +// page or one or more resources failed to load. // See: // https://github.com/gotenberg/gotenberg/issues/913. // https://github.com/gotenberg/gotenberg/issues/959. -func listenForEventLoadingFailed(ctx context.Context, logger *zap.Logger, loadingFailed *error, loadingFailedMu *sync.RWMutex) { +// https://github.com/gotenberg/gotenberg/issues/1021. +func listenForEventLoadingFailed(ctx context.Context, logger *zap.Logger, options eventLoadingFailedOptions) { 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.Type != network.ResourceTypeDocument { - logger.Debug("skip EventLoadingFailed: is not resource type Document") - return - } - - // Supposition: except iframe, an event loading failed with a - // resource type Document is about the main page. - // We are looking for common errors. // TODO: sufficient? errors := []string{ @@ -199,16 +234,35 @@ func listenForEventLoadingFailed(ctx context.Context, logger *zap.Logger, loadin "net::ERR_ADDRESS_UNREACHABLE", "net::ERR_BLOCKED_BY_CLIENT", "net::ERR_BLOCKED_BY_RESPONSE", + "net::ERR_FILE_NOT_FOUND", } if !slices.Contains(errors, ev.ErrorText) { logger.Debug(fmt.Sprintf("skip EventLoadingFailed: '%s' is not part of %+v", ev.ErrorText, errors)) return } - loadingFailedMu.Lock() - defer loadingFailedMu.Unlock() + if ev.Type == network.ResourceTypeDocument { + // Supposition: except iframe, an event loading failed with a + // resource type Document is about the main page. + logger.Debug("event EventLoadingFailed fired for main page") - *loadingFailed = fmt.Errorf("%s", ev.ErrorText) + options.loadingFailedMu.Lock() + defer options.loadingFailedMu.Unlock() + + *options.loadingFailed = fmt.Errorf("%s", ev.ErrorText) + + return + } + + logger.Debug("event EventLoadingFailed fired for a resource") + + options.resourceLoadingFailedMu.Lock() + defer options.resourceLoadingFailedMu.Unlock() + + *options.resourceLoadingFailed = multierr.Append( + *options.resourceLoadingFailed, + fmt.Errorf("resource %s: %s", ev.Type, ev.ErrorText), + ) } }) } diff --git a/pkg/modules/chromium/routes.go b/pkg/modules/chromium/routes.go index a725c7c..4662371 100644 --- a/pkg/modules/chromium/routes.go +++ b/pkg/modules/chromium/routes.go @@ -29,17 +29,19 @@ func FormDataChromiumOptions(ctx *api.Context) (*api.FormData, Options) { defaultOptions := DefaultOptions() var ( - skipNetworkIdleEvent bool - failOnHttpStatusCodes []int64 - failOnConsoleExceptions bool - waitDelay time.Duration - waitWindowStatus string - waitForExpression string - cookies []Cookie - userAgent string - extraHttpHeaders []ExtraHttpHeader - emulatedMediaType string - omitBackground bool + skipNetworkIdleEvent bool + failOnHttpStatusCodes []int64 + failOnResourceHttpStatusCodes []int64 + failOnResourceLoadingFailed bool + failOnConsoleExceptions bool + waitDelay time.Duration + waitWindowStatus string + waitForExpression string + cookies []Cookie + userAgent string + extraHttpHeaders []ExtraHttpHeader + emulatedMediaType string + omitBackground bool ) form := ctx.FormData(). @@ -57,6 +59,20 @@ func FormDataChromiumOptions(ctx *api.Context) (*api.FormData, Options) { return nil }). + Custom("failOnResourceHttpStatusCodes", func(value string) error { + if value == "" { + failOnResourceHttpStatusCodes = defaultOptions.FailOnResourceHttpStatusCodes + return nil + } + + err := json.Unmarshal([]byte(value), &failOnResourceHttpStatusCodes) + if err != nil { + return fmt.Errorf("unmarshal failOnResourceHttpStatusCodes: %w", err) + } + + return nil + }). + Bool("failOnResourceLoadingFailed", &failOnResourceLoadingFailed, defaultOptions.FailOnResourceLoadingFailed). Bool("failOnConsoleExceptions", &failOnConsoleExceptions, defaultOptions.FailOnConsoleExceptions). Duration("waitDelay", &waitDelay, defaultOptions.WaitDelay). String("waitWindowStatus", &waitWindowStatus, defaultOptions.WaitWindowStatus). @@ -158,17 +174,19 @@ func FormDataChromiumOptions(ctx *api.Context) (*api.FormData, Options) { Bool("omitBackground", &omitBackground, defaultOptions.OmitBackground) options := Options{ - SkipNetworkIdleEvent: skipNetworkIdleEvent, - FailOnHttpStatusCodes: failOnHttpStatusCodes, - FailOnConsoleExceptions: failOnConsoleExceptions, - WaitDelay: waitDelay, - WaitWindowStatus: waitWindowStatus, - WaitForExpression: waitForExpression, - Cookies: cookies, - UserAgent: userAgent, - ExtraHttpHeaders: extraHttpHeaders, - EmulatedMediaType: emulatedMediaType, - OmitBackground: omitBackground, + SkipNetworkIdleEvent: skipNetworkIdleEvent, + FailOnHttpStatusCodes: failOnHttpStatusCodes, + FailOnResourceHttpStatusCodes: failOnResourceHttpStatusCodes, + FailOnResourceLoadingFailed: failOnResourceLoadingFailed, + FailOnConsoleExceptions: failOnConsoleExceptions, + WaitDelay: waitDelay, + WaitWindowStatus: waitWindowStatus, + WaitForExpression: waitForExpression, + Cookies: cookies, + UserAgent: userAgent, + ExtraHttpHeaders: extraHttpHeaders, + EmulatedMediaType: emulatedMediaType, + OmitBackground: omitBackground, } return form, options @@ -726,12 +744,22 @@ func handleChromiumError(err error, options Options) error { ) } + if errors.Is(err, ErrInvalidResourceHttpStatusCode) { + return api.WrapError( + err, + api.NewSentinelHttpError( + http.StatusConflict, + fmt.Sprintf("Invalid HTTP status code from resources:\n%s", strings.ReplaceAll(err.Error(), fmt.Sprintf(": %s", ErrInvalidResourceHttpStatusCode.Error()), "")), + ), + ) + } + if errors.Is(err, ErrConsoleExceptions) { return api.WrapError( err, api.NewSentinelHttpError( http.StatusConflict, - fmt.Sprintf("Chromium console exceptions:\n %s", strings.ReplaceAll(err.Error(), ErrConsoleExceptions.Error(), "")), + fmt.Sprintf("Chromium console exceptions:\n%s", strings.ReplaceAll(err.Error(), ErrConsoleExceptions.Error(), "")), ), ) } @@ -746,5 +774,15 @@ func handleChromiumError(err error, options Options) error { ) } + if errors.Is(err, ErrResourceLoadingFailed) { + return api.WrapError( + err, + api.NewSentinelHttpError( + http.StatusConflict, + fmt.Sprintf("Chromium failed to load resources: %v", strings.ReplaceAll(err.Error(), fmt.Sprintf(": %s", ErrResourceLoadingFailed.Error()), "")), + ), + ) + } + return err } diff --git a/pkg/modules/chromium/routes_test.go b/pkg/modules/chromium/routes_test.go index 29fb9d1..629dfbb 100644 --- a/pkg/modules/chromium/routes_test.go +++ b/pkg/modules/chromium/routes_test.go @@ -72,6 +72,44 @@ func TestFormDataChromiumOptions(t *testing.T) { compareWithoutDeepEqual: false, expectValidationError: false, }, + { + scenario: "invalid failOnResourceHttpStatusCodes form field", + ctx: func() *api.ContextMock { + ctx := &api.ContextMock{Context: new(api.Context)} + ctx.SetValues(map[string][]string{ + "failOnResourceHttpStatusCodes": { + "foo", + }, + }) + return ctx + }(), + expectedOptions: func() Options { + options := DefaultOptions() + options.FailOnResourceHttpStatusCodes = nil + return options + }(), + compareWithoutDeepEqual: false, + expectValidationError: true, + }, + { + scenario: "valid failOnResourceHttpStatusCodes form field", + ctx: func() *api.ContextMock { + ctx := &api.ContextMock{Context: new(api.Context)} + ctx.SetValues(map[string][]string{ + "failOnResourceHttpStatusCodes": { + `[399,499,599]`, + }, + }) + return ctx + }(), + expectedOptions: func() Options { + options := DefaultOptions() + options.FailOnResourceHttpStatusCodes = []int64{399, 499, 599} + return options + }(), + compareWithoutDeepEqual: false, + expectValidationError: false, + }, { scenario: "invalid cookies form field", ctx: func() *api.ContextMock { @@ -1592,6 +1630,18 @@ func TestConvertUrl(t *testing.T) { expectHttpStatus: http.StatusConflict, expectOutputPathsCount: 0, }, + { + scenario: "ErrInvalidResourceHttpStatusCode", + ctx: &api.ContextMock{Context: new(api.Context)}, + api: &ApiMock{PdfMock: func(ctx context.Context, logger *zap.Logger, url, outputPath string, options PdfOptions) error { + return ErrInvalidResourceHttpStatusCode + }}, + options: DefaultPdfOptions(), + expectError: true, + expectHttpError: true, + expectHttpStatus: http.StatusConflict, + expectOutputPathsCount: 0, + }, { scenario: "ErrConsoleExceptions", ctx: &api.ContextMock{Context: new(api.Context)}, @@ -1616,6 +1666,18 @@ func TestConvertUrl(t *testing.T) { expectHttpStatus: http.StatusBadRequest, expectOutputPathsCount: 0, }, + { + scenario: "ErrResourceLoadingFailed", + ctx: &api.ContextMock{Context: new(api.Context)}, + api: &ApiMock{PdfMock: func(ctx context.Context, logger *zap.Logger, url, outputPath string, options PdfOptions) error { + return ErrResourceLoadingFailed + }}, + options: DefaultPdfOptions(), + expectError: true, + expectHttpError: true, + expectHttpStatus: http.StatusConflict, + expectOutputPathsCount: 0, + }, { scenario: "error from Chromium", ctx: &api.ContextMock{Context: new(api.Context)}, @@ -1802,6 +1864,18 @@ func TestScreenshotUrl(t *testing.T) { expectHttpStatus: http.StatusConflict, expectOutputPathsCount: 0, }, + { + scenario: "ErrInvalidResourceHttpStatusCode", + ctx: &api.ContextMock{Context: new(api.Context)}, + api: &ApiMock{ScreenshotMock: func(ctx context.Context, logger *zap.Logger, url, outputPath string, options ScreenshotOptions) error { + return ErrInvalidResourceHttpStatusCode + }}, + options: DefaultScreenshotOptions(), + expectError: true, + expectHttpError: true, + expectHttpStatus: http.StatusConflict, + expectOutputPathsCount: 0, + }, { scenario: "ErrConsoleExceptions", ctx: &api.ContextMock{Context: new(api.Context)}, @@ -1826,6 +1900,18 @@ func TestScreenshotUrl(t *testing.T) { expectHttpStatus: http.StatusBadRequest, expectOutputPathsCount: 0, }, + { + scenario: "ErrResourceLoadingFailed", + ctx: &api.ContextMock{Context: new(api.Context)}, + api: &ApiMock{ScreenshotMock: func(ctx context.Context, logger *zap.Logger, url, outputPath string, options ScreenshotOptions) error { + return ErrResourceLoadingFailed + }}, + options: DefaultScreenshotOptions(), + expectError: true, + expectHttpError: true, + expectHttpStatus: http.StatusConflict, + expectOutputPathsCount: 0, + }, { scenario: "error from Chromium", ctx: &api.ContextMock{Context: new(api.Context)}, diff --git a/test/testdata/chromium/html/index.html b/test/testdata/chromium/html/index.html index 29a787b..a19f166 100644 --- a/test/testdata/chromium/html/index.html +++ b/test/testdata/chromium/html/index.html @@ -2,6 +2,12 @@ + + + + + + Gutenberg @@ -23,7 +29,7 @@

It is a press, certainly, but a press from which shall flow in inexhaustible streams...Through it, God will spread His Word. A spring of truth shall flow from it: like a new star it shall scatter the darkness of ignorance, and cause a light heretofore unknown to shine amongst men.

- +
diff --git a/test/testdata/chromium/markdown/index.html b/test/testdata/chromium/markdown/index.html index cedb23a..faa522d 100644 --- a/test/testdata/chromium/markdown/index.html +++ b/test/testdata/chromium/markdown/index.html @@ -2,6 +2,12 @@ + + + + + + Gutenberg @@ -15,7 +21,7 @@

It is a press, certainly, but a press from which shall flow in inexhaustible streams...Through it, God will spread His Word. A spring of truth shall flow from it: like a new star it shall scatter the darkness of ignorance, and cause a light heretofore unknown to shine amongst men.

- +