feat(chromium): add new form field 'failOnConsoleExceptions' (fixes #262)

This commit is contained in:
Julien Neuhart
2021-11-23 15:43:05 +01:00
parent 99ede6477f
commit df98e7512b
6 changed files with 148 additions and 40 deletions

View File

@@ -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: "<html><head></head><body></body></html>",
FooterTemplate: "<html><head></head><body></body></html>",
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: "<html><head></head><body></body></html>",
FooterTemplate: "<html><head></head><body></body></html>",
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)

View File

@@ -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,

View File

@@ -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 {

View File

@@ -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)
}

View File

@@ -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 {