mirror of
https://github.com/gotenberg/gotenberg.git
synced 2026-07-02 00:17:40 +08:00
feat(chromium): add failOnResourceLoadingFailed and failOnResourceHttpStatusCodes form fields
This commit is contained in:
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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("<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",
|
||||
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("<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",
|
||||
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("<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",
|
||||
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("<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",
|
||||
browser: newChromiumBrowser(
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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),
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)},
|
||||
|
||||
+7
-1
@@ -2,6 +2,12 @@
|
||||
<html lang="en">
|
||||
<head>
|
||||
<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 href="https://fonts.googleapis.com/css?family=Montserrat" rel="stylesheet">
|
||||
<title>Gutenberg</title>
|
||||
@@ -23,7 +29,7 @@
|
||||
|
||||
<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>
|
||||
<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>
|
||||
</div>
|
||||
|
||||
|
||||
+7
-1
@@ -2,6 +2,12 @@
|
||||
<html lang="en">
|
||||
<head>
|
||||
<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 href="https://fonts.googleapis.com/css?family=Montserrat" rel="stylesheet">
|
||||
<title>Gutenberg</title>
|
||||
@@ -15,7 +21,7 @@
|
||||
|
||||
<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>
|
||||
<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>
|
||||
</div>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user