diff --git a/pkg/modules/chromium/chromium.go b/pkg/modules/chromium/chromium.go index c8d39bf9..e5ad6f31 100644 --- a/pkg/modules/chromium/chromium.go +++ b/pkg/modules/chromium/chromium.go @@ -15,6 +15,7 @@ import ( "github.com/chromedp/cdproto/fetch" "github.com/chromedp/cdproto/network" "github.com/chromedp/cdproto/page" + "github.com/chromedp/cdproto/runtime" "github.com/chromedp/chromedp" "github.com/gotenberg/gotenberg/v7/pkg/gotenberg" "github.com/gotenberg/gotenberg/v7/pkg/modules/api" @@ -50,6 +51,11 @@ var ( // ErrRpccMessageTooLarge happens when the messages received by // ChromeDevTools are larger than 100 MB. ErrRpccMessageTooLarge = errors.New("rpcc message too large") + + // 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") ) // Chromium is a module which provides both an API and routes for converting @@ -71,6 +77,10 @@ type Chromium struct { // Options are the available options for converting HTML document to PDF. type Options struct { + // FailOnConsoleExceptions sets if the conversion should fail if there are + // exceptions in the Chromium console. + FailOnConsoleExceptions bool + // WaitDelay is the duration to wait when loading an HTML document before // converting it to PDF. // Optional. @@ -167,25 +177,26 @@ type Options struct { // DefaultOptions returns the default values for Options. func DefaultOptions() Options { return Options{ - WaitDelay: 0, - WaitWindowStatus: "", - WaitForExpression: "", - UserAgent: "", - ExtraHTTPHeaders: nil, - EmulatedMediaType: "", - Landscape: false, - PrintBackground: false, - Scale: 1.0, - PaperWidth: 8.5, - PaperHeight: 11, - MarginTop: 0.39, - MarginBottom: 0.39, - MarginLeft: 0.39, - MarginRight: 0.39, - PageRanges: "", - HeaderTemplate: "", - FooterTemplate: "", - PreferCSSPageSize: false, + FailOnConsoleExceptions: false, + WaitDelay: 0, + WaitWindowStatus: "", + WaitForExpression: "", + UserAgent: "", + ExtraHTTPHeaders: nil, + EmulatedMediaType: "", + Landscape: false, + PrintBackground: false, + Scale: 1.0, + PaperWidth: 8.5, + PaperHeight: 11, + MarginTop: 0.39, + MarginBottom: 0.39, + MarginLeft: 0.39, + MarginRight: 0.39, + PageRanges: "", + HeaderTemplate: "", + FooterTemplate: "", + PreferCSSPageSize: false, } } @@ -384,14 +395,25 @@ func (mod Chromium) PDF(ctx context.Context, logger *zap.Logger, URL, outputPath return fmt.Errorf("'%s' matches the expression from the denied list: %w", URL, ErrURLNotAuthorized) } + var ( + consoleExceptions error + consoleExceptionsMu sync.RWMutex + ) + printToPDF := func(URL string, options Options, result *[]byte) chromedp.Tasks { // We validate the underlying requests against our allow / deny lists. // If a request does not pass the validation, we make it fail. listenForEventRequestPaused(taskCtx, logger, mod.allowList, mod.denyList) + // See https://github.com/gotenberg/gotenberg/issues/262. + if options.FailOnConsoleExceptions { + listenForEventExceptionThrown(taskCtx, logger, &consoleExceptions, &consoleExceptionsMu) + } + return chromedp.Tasks{ network.Enable(), fetch.Enable(), + runtime.Enable(), chromedp.ActionFunc(func(ctx context.Context) error { // See https://github.com/gotenberg/gotenberg/issues/175. if !mod.disableJavaScript { @@ -428,8 +450,6 @@ func (mod Chromium) PDF(ctx context.Context, logger *zap.Logger, URL, outputPath return nil } - emulation.SetScriptExecutionDisabled(true) - return fmt.Errorf("set extra HTTP headers: %w", err) }), chromedp.ActionFunc(func(ctx context.Context) error { @@ -646,6 +666,14 @@ func (mod Chromium) PDF(ctx context.Context, logger *zap.Logger, URL, outputPath return fmt.Errorf("chromium PDF: %w", err) } + // See https://github.com/gotenberg/gotenberg/issues/262. + consoleExceptionsMu.RLock() + defer consoleExceptionsMu.RUnlock() + + if consoleExceptions != nil { + return fmt.Errorf("%v: %w", consoleExceptions, ErrConsoleExceptions) + } + err = ioutil.WriteFile(outputPath, buffer, 0600) if err != nil { return fmt.Errorf("write result to output path: %w", err) diff --git a/pkg/modules/chromium/chromium_test.go b/pkg/modules/chromium/chromium_test.go index bde82849..8e92afd5 100644 --- a/pkg/modules/chromium/chromium_test.go +++ b/pkg/modules/chromium/chromium_test.go @@ -257,6 +257,13 @@ func TestChromium_PDF(t *testing.T) { UserAgent: "foo", }, }, + { + URL: "file:///tests/test/testdata/chromium/html/sample10/index.html", + options: Options{ + FailOnConsoleExceptions: true, + }, + expectErr: true, + }, { URL: "file:///tests/test/testdata/chromium/html/sample9/index.html", disableJavaScript: true, diff --git a/pkg/modules/chromium/events.go b/pkg/modules/chromium/events.go index 510a7820..e61733c2 100644 --- a/pkg/modules/chromium/events.go +++ b/pkg/modules/chromium/events.go @@ -4,12 +4,15 @@ import ( "context" "fmt" "regexp" + "sync" "github.com/chromedp/cdproto/cdp" "github.com/chromedp/cdproto/fetch" "github.com/chromedp/cdproto/network" "github.com/chromedp/cdproto/page" + "github.com/chromedp/cdproto/runtime" "github.com/chromedp/chromedp" + "go.uber.org/multierr" "go.uber.org/zap" "golang.org/x/sync/errgroup" ) @@ -59,6 +62,23 @@ func listenForEventRequestPaused(ctx context.Context, logger *zap.Logger, allowL }) } +// listenForEventExceptionThrown listens for exceptions in the console and +// appends those exceptions to the given error pointer. +// See https://github.com/gotenberg/gotenberg/issues/262. +func listenForEventExceptionThrown(ctx context.Context, logger *zap.Logger, consoleExceptions *error, consoleExceptionsMu *sync.RWMutex) { + chromedp.ListenTarget(ctx, func(ev interface{}) { + switch ev := ev.(type) { + case *runtime.EventExceptionThrown: + logger.Debug(fmt.Sprintf("event EventExceptionThrown fired: %+v", ev.ExceptionDetails)) + + consoleExceptionsMu.Lock() + defer consoleExceptionsMu.Unlock() + + *consoleExceptions = multierr.Append(*consoleExceptions, fmt.Errorf("\n%+v", ev.ExceptionDetails)) + } + }) +} + // waitForEventDomContentEventFired waits until the event DomContentEventFired // is fired or the context timeout. func waitForEventDomContentEventFired(ctx context.Context, logger *zap.Logger) func() error { diff --git a/pkg/modules/chromium/routes.go b/pkg/modules/chromium/routes.go index 039d8a93..ef0dc766 100644 --- a/pkg/modules/chromium/routes.go +++ b/pkg/modules/chromium/routes.go @@ -10,6 +10,7 @@ import ( "net/http" "os" "path/filepath" + "strings" "time" "github.com/gotenberg/gotenberg/v7/pkg/gotenberg" @@ -26,6 +27,7 @@ func FormDataChromiumPDFOptions(ctx *api.Context) (*api.FormData, Options) { defaultOptions := DefaultOptions() var ( + failOnConsoleExceptions bool waitDelay time.Duration waitWindowStatus string waitForExpression string @@ -41,6 +43,7 @@ func FormDataChromiumPDFOptions(ctx *api.Context) (*api.FormData, Options) { ) form := ctx.FormData(). + Bool("failOnConsoleExceptions", &failOnConsoleExceptions, defaultOptions.FailOnConsoleExceptions). Duration("waitDelay", &waitDelay, defaultOptions.WaitDelay). String("waitWindowStatus", &waitWindowStatus, defaultOptions.WaitWindowStatus). String("waitForExpression", &waitForExpression, defaultOptions.WaitForExpression). @@ -75,25 +78,26 @@ func FormDataChromiumPDFOptions(ctx *api.Context) (*api.FormData, Options) { Bool("preferCssPageSize", &preferCSSPageSize, defaultOptions.PreferCSSPageSize) options := Options{ - WaitDelay: waitDelay, - WaitWindowStatus: waitWindowStatus, - WaitForExpression: waitForExpression, - UserAgent: userAgent, - ExtraHTTPHeaders: extraHTTPHeaders, - EmulatedMediaType: emulatedMediaType, - Landscape: landscape, - PrintBackground: printBackground, - Scale: scale, - PaperWidth: paperWidth, - PaperHeight: paperHeight, - MarginTop: marginTop, - MarginBottom: marginBottom, - MarginLeft: marginLeft, - MarginRight: marginRight, - PageRanges: pageRanges, - HeaderTemplate: headerTemplate, - FooterTemplate: footerTemplate, - PreferCSSPageSize: preferCSSPageSize, + FailOnConsoleExceptions: failOnConsoleExceptions, + WaitDelay: waitDelay, + WaitWindowStatus: waitWindowStatus, + WaitForExpression: waitForExpression, + UserAgent: userAgent, + ExtraHTTPHeaders: extraHTTPHeaders, + EmulatedMediaType: emulatedMediaType, + Landscape: landscape, + PrintBackground: printBackground, + Scale: scale, + PaperWidth: paperWidth, + PaperHeight: paperHeight, + MarginTop: marginTop, + MarginBottom: marginBottom, + MarginLeft: marginLeft, + MarginRight: marginRight, + PageRanges: pageRanges, + HeaderTemplate: headerTemplate, + FooterTemplate: footerTemplate, + PreferCSSPageSize: preferCSSPageSize, } return form, options @@ -344,6 +348,16 @@ func convertURL(ctx *api.Context, chromium API, engine gotenberg.PDFEngine, URL, ) } + if errors.Is(err, ErrConsoleExceptions) { + return api.WrapError( + fmt.Errorf("convert to PDF: %w", err), + api.NewSentinelHTTPError( + http.StatusConflict, + fmt.Sprintf("Chromium console exceptions:\n %s", strings.ReplaceAll(err.Error(), ErrConsoleExceptions.Error(), "")), + ), + ) + } + return fmt.Errorf("convert to PDF: %w", err) } diff --git a/pkg/modules/chromium/routes_test.go b/pkg/modules/chromium/routes_test.go index 0ee95bce..7e22195c 100644 --- a/pkg/modules/chromium/routes_test.go +++ b/pkg/modules/chromium/routes_test.go @@ -622,6 +622,21 @@ func TestConvertURL(t *testing.T) { expectHTTPErr: true, expectHTTPStatus: http.StatusBadRequest, }, + { + ctx: &api.MockContext{Context: &api.Context{}}, + api: func() API { + chromiumAPI := struct{ ProtoAPI }{} + chromiumAPI.pdf = func(_ context.Context, _ *zap.Logger, _, _ string, _ Options) error { + return ErrConsoleExceptions + } + + return chromiumAPI + }(), + options: DefaultOptions(), + expectErr: true, + expectHTTPErr: true, + expectHTTPStatus: http.StatusConflict, + }, { ctx: &api.MockContext{Context: &api.Context{}}, api: func() API { diff --git a/test/testdata/chromium/html/sample10/index.html b/test/testdata/chromium/html/sample10/index.html new file mode 100644 index 00000000..de619284 --- /dev/null +++ b/test/testdata/chromium/html/sample10/index.html @@ -0,0 +1,24 @@ + + + + + Gutenberg + + + +

Console API

+ + + + + + \ No newline at end of file