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/fetch"
"github.com/chromedp/cdproto/network" "github.com/chromedp/cdproto/network"
"github.com/chromedp/cdproto/page" "github.com/chromedp/cdproto/page"
"github.com/chromedp/cdproto/runtime"
"github.com/chromedp/chromedp" "github.com/chromedp/chromedp"
"github.com/gotenberg/gotenberg/v7/pkg/gotenberg" "github.com/gotenberg/gotenberg/v7/pkg/gotenberg"
"github.com/gotenberg/gotenberg/v7/pkg/modules/api" "github.com/gotenberg/gotenberg/v7/pkg/modules/api"
@@ -50,6 +51,11 @@ var (
// ErrRpccMessageTooLarge happens when the messages received by // ErrRpccMessageTooLarge happens when the messages received by
// ChromeDevTools are larger than 100 MB. // ChromeDevTools are larger than 100 MB.
ErrRpccMessageTooLarge = errors.New("rpcc message too large") 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 // 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. // Options are the available options for converting HTML document to PDF.
type Options struct { 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 // WaitDelay is the duration to wait when loading an HTML document before
// converting it to PDF. // converting it to PDF.
// Optional. // Optional.
@@ -167,25 +177,26 @@ type Options struct {
// DefaultOptions returns the default values for Options. // DefaultOptions returns the default values for Options.
func DefaultOptions() Options { func DefaultOptions() Options {
return Options{ return Options{
WaitDelay: 0, FailOnConsoleExceptions: false,
WaitWindowStatus: "", WaitDelay: 0,
WaitForExpression: "", WaitWindowStatus: "",
UserAgent: "", WaitForExpression: "",
ExtraHTTPHeaders: nil, UserAgent: "",
EmulatedMediaType: "", ExtraHTTPHeaders: nil,
Landscape: false, EmulatedMediaType: "",
PrintBackground: false, Landscape: false,
Scale: 1.0, PrintBackground: false,
PaperWidth: 8.5, Scale: 1.0,
PaperHeight: 11, PaperWidth: 8.5,
MarginTop: 0.39, PaperHeight: 11,
MarginBottom: 0.39, MarginTop: 0.39,
MarginLeft: 0.39, MarginBottom: 0.39,
MarginRight: 0.39, MarginLeft: 0.39,
PageRanges: "", MarginRight: 0.39,
HeaderTemplate: "<html><head></head><body></body></html>", PageRanges: "",
FooterTemplate: "<html><head></head><body></body></html>", HeaderTemplate: "<html><head></head><body></body></html>",
PreferCSSPageSize: false, 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) 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 { printToPDF := func(URL string, options Options, result *[]byte) chromedp.Tasks {
// We validate the underlying requests against our allow / deny lists. // We validate the underlying requests against our allow / deny lists.
// If a request does not pass the validation, we make it fail. // If a request does not pass the validation, we make it fail.
listenForEventRequestPaused(taskCtx, logger, mod.allowList, mod.denyList) 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{ return chromedp.Tasks{
network.Enable(), network.Enable(),
fetch.Enable(), fetch.Enable(),
runtime.Enable(),
chromedp.ActionFunc(func(ctx context.Context) error { chromedp.ActionFunc(func(ctx context.Context) error {
// See https://github.com/gotenberg/gotenberg/issues/175. // See https://github.com/gotenberg/gotenberg/issues/175.
if !mod.disableJavaScript { if !mod.disableJavaScript {
@@ -428,8 +450,6 @@ func (mod Chromium) PDF(ctx context.Context, logger *zap.Logger, URL, outputPath
return nil return nil
} }
emulation.SetScriptExecutionDisabled(true)
return fmt.Errorf("set extra HTTP headers: %w", err) return fmt.Errorf("set extra HTTP headers: %w", err)
}), }),
chromedp.ActionFunc(func(ctx context.Context) error { 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) 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) err = ioutil.WriteFile(outputPath, buffer, 0600)
if err != nil { if err != nil {
return fmt.Errorf("write result to output path: %w", err) return fmt.Errorf("write result to output path: %w", err)

View File

@@ -257,6 +257,13 @@ func TestChromium_PDF(t *testing.T) {
UserAgent: "foo", 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", URL: "file:///tests/test/testdata/chromium/html/sample9/index.html",
disableJavaScript: true, disableJavaScript: true,

View File

@@ -4,12 +4,15 @@ import (
"context" "context"
"fmt" "fmt"
"regexp" "regexp"
"sync"
"github.com/chromedp/cdproto/cdp" "github.com/chromedp/cdproto/cdp"
"github.com/chromedp/cdproto/fetch" "github.com/chromedp/cdproto/fetch"
"github.com/chromedp/cdproto/network" "github.com/chromedp/cdproto/network"
"github.com/chromedp/cdproto/page" "github.com/chromedp/cdproto/page"
"github.com/chromedp/cdproto/runtime"
"github.com/chromedp/chromedp" "github.com/chromedp/chromedp"
"go.uber.org/multierr"
"go.uber.org/zap" "go.uber.org/zap"
"golang.org/x/sync/errgroup" "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 // waitForEventDomContentEventFired waits until the event DomContentEventFired
// is fired or the context timeout. // is fired or the context timeout.
func waitForEventDomContentEventFired(ctx context.Context, logger *zap.Logger) func() error { func waitForEventDomContentEventFired(ctx context.Context, logger *zap.Logger) func() error {

View File

@@ -10,6 +10,7 @@ import (
"net/http" "net/http"
"os" "os"
"path/filepath" "path/filepath"
"strings"
"time" "time"
"github.com/gotenberg/gotenberg/v7/pkg/gotenberg" "github.com/gotenberg/gotenberg/v7/pkg/gotenberg"
@@ -26,6 +27,7 @@ func FormDataChromiumPDFOptions(ctx *api.Context) (*api.FormData, Options) {
defaultOptions := DefaultOptions() defaultOptions := DefaultOptions()
var ( var (
failOnConsoleExceptions bool
waitDelay time.Duration waitDelay time.Duration
waitWindowStatus string waitWindowStatus string
waitForExpression string waitForExpression string
@@ -41,6 +43,7 @@ func FormDataChromiumPDFOptions(ctx *api.Context) (*api.FormData, Options) {
) )
form := ctx.FormData(). form := ctx.FormData().
Bool("failOnConsoleExceptions", &failOnConsoleExceptions, defaultOptions.FailOnConsoleExceptions).
Duration("waitDelay", &waitDelay, defaultOptions.WaitDelay). Duration("waitDelay", &waitDelay, defaultOptions.WaitDelay).
String("waitWindowStatus", &waitWindowStatus, defaultOptions.WaitWindowStatus). String("waitWindowStatus", &waitWindowStatus, defaultOptions.WaitWindowStatus).
String("waitForExpression", &waitForExpression, defaultOptions.WaitForExpression). String("waitForExpression", &waitForExpression, defaultOptions.WaitForExpression).
@@ -75,25 +78,26 @@ func FormDataChromiumPDFOptions(ctx *api.Context) (*api.FormData, Options) {
Bool("preferCssPageSize", &preferCSSPageSize, defaultOptions.PreferCSSPageSize) Bool("preferCssPageSize", &preferCSSPageSize, defaultOptions.PreferCSSPageSize)
options := Options{ options := Options{
WaitDelay: waitDelay, FailOnConsoleExceptions: failOnConsoleExceptions,
WaitWindowStatus: waitWindowStatus, WaitDelay: waitDelay,
WaitForExpression: waitForExpression, WaitWindowStatus: waitWindowStatus,
UserAgent: userAgent, WaitForExpression: waitForExpression,
ExtraHTTPHeaders: extraHTTPHeaders, UserAgent: userAgent,
EmulatedMediaType: emulatedMediaType, ExtraHTTPHeaders: extraHTTPHeaders,
Landscape: landscape, EmulatedMediaType: emulatedMediaType,
PrintBackground: printBackground, Landscape: landscape,
Scale: scale, PrintBackground: printBackground,
PaperWidth: paperWidth, Scale: scale,
PaperHeight: paperHeight, PaperWidth: paperWidth,
MarginTop: marginTop, PaperHeight: paperHeight,
MarginBottom: marginBottom, MarginTop: marginTop,
MarginLeft: marginLeft, MarginBottom: marginBottom,
MarginRight: marginRight, MarginLeft: marginLeft,
PageRanges: pageRanges, MarginRight: marginRight,
HeaderTemplate: headerTemplate, PageRanges: pageRanges,
FooterTemplate: footerTemplate, HeaderTemplate: headerTemplate,
PreferCSSPageSize: preferCSSPageSize, FooterTemplate: footerTemplate,
PreferCSSPageSize: preferCSSPageSize,
} }
return form, options 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) return fmt.Errorf("convert to PDF: %w", err)
} }

View File

@@ -622,6 +622,21 @@ func TestConvertURL(t *testing.T) {
expectHTTPErr: true, expectHTTPErr: true,
expectHTTPStatus: http.StatusBadRequest, 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{}}, ctx: &api.MockContext{Context: &api.Context{}},
api: func() API { api: func() API {

View File

@@ -0,0 +1,24 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Gutenberg</title>
</head>
<body>
<p>Console API</p>
<script type="application/javascript">
console.log("a simple message")
console.debug("a debug message")
console.warn("a warning message")
console.error("an error message")
</script>
<script type="application/javascript">
throw new Error("Exception 1")
</script>
<script type="application/javascript">
throw new Error("Exception 2")
</script>
</body>
</html>