feat(chromium): add failOnResourceLoadingFailed and failOnResourceHttpStatusCodes form fields

This commit is contained in:
Julien Neuhart
2024-11-05 11:44:01 +01:00
parent b2f957d1d8
commit 711b43070d
8 changed files with 458 additions and 74 deletions

View File

@@ -299,13 +299,25 @@ func (b *chromiumBrowser) do(ctx context.Context, logger *zap.Logger, url string
}) })
var ( var (
invalidHttpStatusCode error invalidHttpStatusCode error
invalidHttpStatusCodeMu sync.RWMutex invalidHttpStatusCodeMu sync.RWMutex
invalidResourceHttpStatusCode error
invalidResourceHttpStatusCodeMu sync.RWMutex
) )
// See https://github.com/gotenberg/gotenberg/issues/613. // See:
if len(options.FailOnHttpStatusCodes) != 0 { // https://github.com/gotenberg/gotenberg/issues/613.
listenForEventResponseReceived(taskCtx, logger, url, options.FailOnHttpStatusCodes, &invalidHttpStatusCode, &invalidHttpStatusCodeMu) // 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 ( var (
@@ -319,14 +331,22 @@ func (b *chromiumBrowser) do(ctx context.Context, logger *zap.Logger, url string
} }
var ( var (
loadingFailed error loadingFailed error
loadingFailedMu sync.RWMutex loadingFailedMu sync.RWMutex
resourceLoadingFailed error
resourceLoadingFailedMu sync.RWMutex
) )
// See: // See:
// https://github.com/gotenberg/gotenberg/issues/913 // https://github.com/gotenberg/gotenberg/issues/913.
// https://github.com/gotenberg/gotenberg/issues/959 // https://github.com/gotenberg/gotenberg/issues/959.
listenForEventLoadingFailed(taskCtx, logger, &loadingFailed, &loadingFailedMu) // https://github.com/gotenberg/gotenberg/issues/1021.
listenForEventLoadingFailed(taskCtx, logger, eventLoadingFailedOptions{
loadingFailed: &loadingFailed,
loadingFailedMu: &loadingFailedMu,
resourceLoadingFailed: &resourceLoadingFailed,
resourceLoadingFailedMu: &resourceLoadingFailedMu,
})
err = chromedp.Run(taskCtx, tasks...) err = chromedp.Run(taskCtx, tasks...)
if err != nil { 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) 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. // See https://github.com/gotenberg/gotenberg/issues/262.
consoleExceptionsMu.RLock() consoleExceptionsMu.RLock()
defer consoleExceptionsMu.RUnlock() defer consoleExceptionsMu.RUnlock()
@@ -364,8 +392,8 @@ func (b *chromiumBrowser) do(ctx context.Context, logger *zap.Logger, url string
} }
// See: // See:
// https://github.com/gotenberg/gotenberg/issues/913 // https://github.com/gotenberg/gotenberg/issues/913.
// https://github.com/gotenberg/gotenberg/issues/959 // https://github.com/gotenberg/gotenberg/issues/959.
loadingFailedMu.RLock() loadingFailedMu.RLock()
defer loadingFailedMu.RUnlock() 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) 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 return nil
} }

View File

@@ -446,6 +446,44 @@ func TestChromiumBrowser_pdf(t *testing.T) {
expectError: true, expectError: true,
expectedError: ErrInvalidHttpStatusCode, 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("<html><head><link rel=\"stylesheet\" type=\"text/css\" href=\"style.css\"></head><body><h1>ErrInvalidResourceHttpStatusCode</h1></body></html>"), 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", scenario: "ErrConsoleExceptions",
browser: newChromiumBrowser( browser: newChromiumBrowser(
@@ -505,6 +543,39 @@ func TestChromiumBrowser_pdf(t *testing.T) {
expectError: true, expectError: true,
expectedError: ErrLoadingFailed, 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("<html><head><link rel=\"stylesheet\" type=\"text/css\" href=\"http://localhost:100/style.css\"></head><body><h1>ErrResourceLoadingFailed</h1></body></html>"), 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", scenario: "clear cache",
browser: newChromiumBrowser( browser: newChromiumBrowser(
@@ -1537,6 +1608,44 @@ func TestChromiumBrowser_screenshot(t *testing.T) {
expectError: true, expectError: true,
expectedError: ErrInvalidHttpStatusCode, 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("<html><head><link rel=\"stylesheet\" type=\"text/css\" href=\"style.css\"></head><body><h1>ErrInvalidResourceHttpStatusCode</h1></body></html>"), 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", scenario: "ErrConsoleExceptions",
browser: newChromiumBrowser( browser: newChromiumBrowser(
@@ -1580,7 +1689,6 @@ func TestChromiumBrowser_screenshot(t *testing.T) {
denyList: regexp2.MustCompile("", 0), denyList: regexp2.MustCompile("", 0),
}, },
), ),
fs: func() *gotenberg.FileSystem { fs: func() *gotenberg.FileSystem {
fs := gotenberg.NewFileSystem() fs := gotenberg.NewFileSystem()
@@ -1597,6 +1705,39 @@ func TestChromiumBrowser_screenshot(t *testing.T) {
expectError: true, expectError: true,
expectedError: ErrLoadingFailed, 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("<html><head><link rel=\"stylesheet\" type=\"text/css\" href=\"http://localhost:100/style.css\"></head><body><h1>ErrResourceLoadingFailed</h1></body></html>"), 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", scenario: "clear cache",
browser: newChromiumBrowser( browser: newChromiumBrowser(

View File

@@ -38,14 +38,22 @@ var (
// matches with one of the entry in [Options.FailOnHttpStatusCodes]. // matches with one of the entry in [Options.FailOnHttpStatusCodes].
ErrInvalidHttpStatusCode = errors.New("invalid HTTP status code") 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 // ErrConsoleExceptions happens when there are exceptions in the Chromium
// console. It also happens only if the [Options.FailOnConsoleExceptions] // console. It also happens only if the [Options.FailOnConsoleExceptions]
// is set to true. // is set to true.
ErrConsoleExceptions = errors.New("console exceptions") 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") ErrLoadingFailed = errors.New("loading failed")
// ErrResourceLoadingFailed happens when one or more resources failed to load.
ErrResourceLoadingFailed = errors.New("resource loading failed")
// PDF specific. // PDF specific.
// ErrOmitBackgroundWithoutPrintBackground happens if // ErrOmitBackgroundWithoutPrintBackground happens if
@@ -86,6 +94,14 @@ type Options struct {
// code from the main page matches with one of its entries. // code from the main page matches with one of its entries.
FailOnHttpStatusCodes []int64 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 // FailOnConsoleExceptions sets if the conversion should fail if there are
// exceptions in the Chromium console. // exceptions in the Chromium console.
FailOnConsoleExceptions bool FailOnConsoleExceptions bool
@@ -124,17 +140,19 @@ 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{
SkipNetworkIdleEvent: true, SkipNetworkIdleEvent: true,
FailOnHttpStatusCodes: []int64{499, 599}, FailOnHttpStatusCodes: []int64{499, 599},
FailOnConsoleExceptions: false, FailOnResourceHttpStatusCodes: nil,
WaitDelay: 0, FailOnResourceLoadingFailed: false,
WaitWindowStatus: "", FailOnConsoleExceptions: false,
WaitForExpression: "", WaitDelay: 0,
Cookies: nil, WaitWindowStatus: "",
UserAgent: "", WaitForExpression: "",
ExtraHttpHeaders: nil, Cookies: nil,
EmulatedMediaType: "", UserAgent: "",
OmitBackground: false, ExtraHttpHeaders: nil,
EmulatedMediaType: "",
OmitBackground: false,
} }
} }

View File

@@ -3,6 +3,7 @@ package chromium
import ( import (
"context" "context"
"fmt" "fmt"
"net/http"
"slices" "slices"
"sync" "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 // listenForEventResponseReceived listens for an invalid HTTP status code that
// is returned by the main page. // is returned by the main page or by one or more resources.
// See https://github.com/gotenberg/gotenberg/issues/613. // See:
func listenForEventResponseReceived(ctx context.Context, logger *zap.Logger, url string, failOnHttpStatusCodes []int64, invalidHttpStatusCode *error, invalidHttpStatusCodeMu *sync.RWMutex) { // 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} { 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++ { 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{}) { chromedp.ListenTarget(ctx, func(ev interface{}) {
switch ev := ev.(type) { switch ev := ev.(type) {
case *network.EventResponseReceived: 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 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) { if slices.Contains(options.failOnResourceOnHttpStatusCode, ev.Response.Status) {
invalidHttpStatusCodeMu.Lock() options.invalidResourceHttpStatusCodeMu.Lock()
defer invalidHttpStatusCodeMu.Unlock() 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 // listenForEventLoadingFailed listens for an event indicating that the main
// page failed to load. // page or one or more resources failed to load.
// See: // See:
// https://github.com/gotenberg/gotenberg/issues/913. // https://github.com/gotenberg/gotenberg/issues/913.
// https://github.com/gotenberg/gotenberg/issues/959. // 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{}) { chromedp.ListenTarget(ctx, func(ev interface{}) {
switch ev := ev.(type) { switch ev := ev.(type) {
case *network.EventLoadingFailed: case *network.EventLoadingFailed:
logger.Debug(fmt.Sprintf("event EventLoadingFailed fired: %+v", ev.ErrorText)) 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. // We are looking for common errors.
// TODO: sufficient? // TODO: sufficient?
errors := []string{ errors := []string{
@@ -199,16 +234,35 @@ func listenForEventLoadingFailed(ctx context.Context, logger *zap.Logger, loadin
"net::ERR_ADDRESS_UNREACHABLE", "net::ERR_ADDRESS_UNREACHABLE",
"net::ERR_BLOCKED_BY_CLIENT", "net::ERR_BLOCKED_BY_CLIENT",
"net::ERR_BLOCKED_BY_RESPONSE", "net::ERR_BLOCKED_BY_RESPONSE",
"net::ERR_FILE_NOT_FOUND",
} }
if !slices.Contains(errors, ev.ErrorText) { if !slices.Contains(errors, ev.ErrorText) {
logger.Debug(fmt.Sprintf("skip EventLoadingFailed: '%s' is not part of %+v", ev.ErrorText, errors)) logger.Debug(fmt.Sprintf("skip EventLoadingFailed: '%s' is not part of %+v", ev.ErrorText, errors))
return return
} }
loadingFailedMu.Lock() if ev.Type == network.ResourceTypeDocument {
defer loadingFailedMu.Unlock() // 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),
)
} }
}) })
} }

View File

@@ -29,17 +29,19 @@ func FormDataChromiumOptions(ctx *api.Context) (*api.FormData, Options) {
defaultOptions := DefaultOptions() defaultOptions := DefaultOptions()
var ( var (
skipNetworkIdleEvent bool skipNetworkIdleEvent bool
failOnHttpStatusCodes []int64 failOnHttpStatusCodes []int64
failOnConsoleExceptions bool failOnResourceHttpStatusCodes []int64
waitDelay time.Duration failOnResourceLoadingFailed bool
waitWindowStatus string failOnConsoleExceptions bool
waitForExpression string waitDelay time.Duration
cookies []Cookie waitWindowStatus string
userAgent string waitForExpression string
extraHttpHeaders []ExtraHttpHeader cookies []Cookie
emulatedMediaType string userAgent string
omitBackground bool extraHttpHeaders []ExtraHttpHeader
emulatedMediaType string
omitBackground bool
) )
form := ctx.FormData(). form := ctx.FormData().
@@ -57,6 +59,20 @@ func FormDataChromiumOptions(ctx *api.Context) (*api.FormData, Options) {
return nil 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). 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).
@@ -158,17 +174,19 @@ func FormDataChromiumOptions(ctx *api.Context) (*api.FormData, Options) {
Bool("omitBackground", &omitBackground, defaultOptions.OmitBackground) Bool("omitBackground", &omitBackground, defaultOptions.OmitBackground)
options := Options{ options := Options{
SkipNetworkIdleEvent: skipNetworkIdleEvent, SkipNetworkIdleEvent: skipNetworkIdleEvent,
FailOnHttpStatusCodes: failOnHttpStatusCodes, FailOnHttpStatusCodes: failOnHttpStatusCodes,
FailOnConsoleExceptions: failOnConsoleExceptions, FailOnResourceHttpStatusCodes: failOnResourceHttpStatusCodes,
WaitDelay: waitDelay, FailOnResourceLoadingFailed: failOnResourceLoadingFailed,
WaitWindowStatus: waitWindowStatus, FailOnConsoleExceptions: failOnConsoleExceptions,
WaitForExpression: waitForExpression, WaitDelay: waitDelay,
Cookies: cookies, WaitWindowStatus: waitWindowStatus,
UserAgent: userAgent, WaitForExpression: waitForExpression,
ExtraHttpHeaders: extraHttpHeaders, Cookies: cookies,
EmulatedMediaType: emulatedMediaType, UserAgent: userAgent,
OmitBackground: omitBackground, ExtraHttpHeaders: extraHttpHeaders,
EmulatedMediaType: emulatedMediaType,
OmitBackground: omitBackground,
} }
return form, options 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) { if errors.Is(err, ErrConsoleExceptions) {
return api.WrapError( return api.WrapError(
err, err,
api.NewSentinelHttpError( api.NewSentinelHttpError(
http.StatusConflict, 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 return err
} }

View File

@@ -72,6 +72,44 @@ func TestFormDataChromiumOptions(t *testing.T) {
compareWithoutDeepEqual: false, compareWithoutDeepEqual: false,
expectValidationError: 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", scenario: "invalid cookies form field",
ctx: func() *api.ContextMock { ctx: func() *api.ContextMock {
@@ -1592,6 +1630,18 @@ func TestConvertUrl(t *testing.T) {
expectHttpStatus: http.StatusConflict, expectHttpStatus: http.StatusConflict,
expectOutputPathsCount: 0, 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", scenario: "ErrConsoleExceptions",
ctx: &api.ContextMock{Context: new(api.Context)}, ctx: &api.ContextMock{Context: new(api.Context)},
@@ -1616,6 +1666,18 @@ func TestConvertUrl(t *testing.T) {
expectHttpStatus: http.StatusBadRequest, expectHttpStatus: http.StatusBadRequest,
expectOutputPathsCount: 0, 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", scenario: "error from Chromium",
ctx: &api.ContextMock{Context: new(api.Context)}, ctx: &api.ContextMock{Context: new(api.Context)},
@@ -1802,6 +1864,18 @@ func TestScreenshotUrl(t *testing.T) {
expectHttpStatus: http.StatusConflict, expectHttpStatus: http.StatusConflict,
expectOutputPathsCount: 0, 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", scenario: "ErrConsoleExceptions",
ctx: &api.ContextMock{Context: new(api.Context)}, ctx: &api.ContextMock{Context: new(api.Context)},
@@ -1826,6 +1900,18 @@ func TestScreenshotUrl(t *testing.T) {
expectHttpStatus: http.StatusBadRequest, expectHttpStatus: http.StatusBadRequest,
expectOutputPathsCount: 0, 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", scenario: "error from Chromium",
ctx: &api.ContextMock{Context: new(api.Context)}, ctx: &api.ContextMock{Context: new(api.Context)},

View File

@@ -2,6 +2,12 @@
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<!-- net::ERR_FILE_NOT_FOUND -->
<link rel="stylesheet" type="text/css" href="doesnotexist.css">
<!-- net::ERR_CONNECTION_REFUSED -->
<link rel="stylesheet" type="text/css" href="http://localhost:100/style.css">
<!-- 400 Bad Request -->
<link rel="stylesheet" type="text/css" href="https://httpstat.us/400">
<link rel="stylesheet" type="text/css" href="style.css"> <link rel="stylesheet" type="text/css" href="style.css">
<link href="https://fonts.googleapis.com/css?family=Montserrat" rel="stylesheet"> <link href="https://fonts.googleapis.com/css?family=Montserrat" rel="stylesheet">
<title>Gutenberg</title> <title>Gutenberg</title>
@@ -23,7 +29,7 @@
<blockquote cite="https://sites.google.com/site/johanngutenbergper5/q"> <blockquote cite="https://sites.google.com/site/johanngutenbergper5/q">
<p>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.</p> <p>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.</p>
<footer><a href="https://sites.google.com/site/johanngutenbergper5/q">Johannes Gutenberg</a></cite></footer> <footer><a href="https://sites.google.com/site/johanngutenbergper5/q">Johannes Gutenberg</a></footer>
</blockquote> </blockquote>
</div> </div>

View File

@@ -2,6 +2,12 @@
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<!-- net::ERR_FILE_NOT_FOUND -->
<link rel="stylesheet" type="text/css" href="doesnotexist.css">
<!-- net::ERR_CONNECTION_REFUSED -->
<link rel="stylesheet" type="text/css" href="http://localhost:100/style.css">
<!-- 400 Bad Request -->
<link rel="stylesheet" type="text/css" href="https://httpstat.us/400">
<link rel="stylesheet" type="text/css" href="style.css"> <link rel="stylesheet" type="text/css" href="style.css">
<link href="https://fonts.googleapis.com/css?family=Montserrat" rel="stylesheet"> <link href="https://fonts.googleapis.com/css?family=Montserrat" rel="stylesheet">
<title>Gutenberg</title> <title>Gutenberg</title>
@@ -15,7 +21,7 @@
<blockquote cite="https://sites.google.com/site/johanngutenbergper5/q"> <blockquote cite="https://sites.google.com/site/johanngutenbergper5/q">
<p>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.</p> <p>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.</p>
<footer><a href="https://sites.google.com/site/johanngutenbergper5/q">Johannes Gutenberg</a></cite></footer> <footer><a href="https://sites.google.com/site/johanngutenbergper5/q">Johannes Gutenberg</a></footer>
</blockquote> </blockquote>
</div> </div>