diff --git a/pkg/modules/chromium/browser.go b/pkg/modules/chromium/browser.go
index c7b105ae..4e895203 100644
--- a/pkg/modules/chromium/browser.go
+++ b/pkg/modules/chromium/browser.go
@@ -228,7 +228,6 @@ func (b *chromiumBrowser) pdf(ctx context.Context, logger *zap.Logger, url, outp
disableJavaScriptActionFunc(logger, b.arguments.disableJavaScript),
setCookiesActionFunc(logger, options.Cookies),
userAgentOverride(logger, options.UserAgent),
- extraHttpHeadersActionFunc(logger, options.ExtraHttpHeaders),
navigateActionFunc(logger, url, options.SkipNetworkIdleEvent),
hideDefaultWhiteBackgroundActionFunc(logger, options.OmitBackground, options.PrintBackground),
forceExactColorsActionFunc(),
@@ -252,7 +251,6 @@ func (b *chromiumBrowser) screenshot(ctx context.Context, logger *zap.Logger, ur
disableJavaScriptActionFunc(logger, b.arguments.disableJavaScript),
setCookiesActionFunc(logger, options.Cookies),
userAgentOverride(logger, options.UserAgent),
- extraHttpHeadersActionFunc(logger, options.ExtraHttpHeaders),
navigateActionFunc(logger, url, options.SkipNetworkIdleEvent),
hideDefaultWhiteBackgroundActionFunc(logger, options.OmitBackground, true),
forceExactColorsActionFunc(),
@@ -291,8 +289,14 @@ func (b *chromiumBrowser) do(ctx context.Context, logger *zap.Logger, url string
defer taskCancel()
// We validate all others requests against our allow / deny lists.
- // If a request does not pass the validation, we make it fail.
- listenForEventRequestPaused(taskCtx, logger, b.arguments.allowList, b.arguments.denyList)
+ // If a request does not pass the validation, we make it fail. It also set
+ // the extra HTTP headers, if any.
+ // See https://github.com/gotenberg/gotenberg/issues/1011.
+ listenForEventRequestPaused(taskCtx, logger, eventRequestPausedOptions{
+ allowList: b.arguments.allowList,
+ denyList: b.arguments.denyList,
+ extraHttpHeaders: options.ExtraHttpHeaders,
+ })
var (
invalidHttpStatusCode error
diff --git a/pkg/modules/chromium/browser_test.go b/pkg/modules/chromium/browser_test.go
index dfee69ab..9cc4bcb0 100644
--- a/pkg/modules/chromium/browser_test.go
+++ b/pkg/modules/chromium/browser_test.go
@@ -702,8 +702,21 @@ func TestChromiumBrowser_pdf(t *testing.T) {
return fs
}(),
options: PdfOptions{
- Options: Options{ExtraHttpHeaders: map[string]string{
- "X-Foo": "Bar",
+ Options: Options{ExtraHttpHeaders: []ExtraHttpHeader{
+ {
+ Name: "X-Foo",
+ Value: "foo",
+ },
+ {
+ Name: "X-Bar",
+ Value: "bar",
+ Scope: regexp2.MustCompile(`.*index\.html.*`, 0),
+ },
+ {
+ Name: "X-Baz",
+ Value: "baz",
+ Scope: regexp2.MustCompile(`.*another\.html.*`, 0),
+ },
}},
},
noDeadline: false,
@@ -711,6 +724,10 @@ func TestChromiumBrowser_pdf(t *testing.T) {
expectError: false,
expectedLogEntries: []string{
"extra HTTP headers:",
+ "extra HTTP header 'X-Foo' will be set for request URL",
+ "extra HTTP header 'X-Bar' (scoped) will be set for request URL",
+ "extra HTTP header 'X-Baz' (scoped) will not be set for request URL",
+ "setting extra HTTP headers for request URL",
},
},
{
@@ -1716,6 +1733,41 @@ func TestChromiumBrowser_screenshot(t *testing.T) {
"set cookie",
},
},
+ {
+ scenario: "user agent override",
+ 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("
User-Agent override
"), 0o755)
+ if err != nil {
+ t.Fatalf("expected no error but got: %v", err)
+ }
+
+ return fs
+ }(),
+ options: ScreenshotOptions{
+ Options: Options{UserAgent: "foo"},
+ },
+ noDeadline: false,
+ start: true,
+ expectError: false,
+ expectedLogEntries: []string{
+ fmt.Sprintf("user agent override: foo"),
+ },
+ },
{
scenario: "extra HTTP headers",
browser: newChromiumBrowser(
@@ -1742,8 +1794,21 @@ func TestChromiumBrowser_screenshot(t *testing.T) {
return fs
}(),
options: ScreenshotOptions{
- Options: Options{ExtraHttpHeaders: map[string]string{
- "X-Foo": "Bar",
+ Options: Options{ExtraHttpHeaders: []ExtraHttpHeader{
+ {
+ Name: "X-Foo",
+ Value: "foo",
+ },
+ {
+ Name: "X-Bar",
+ Value: "bar",
+ Scope: regexp2.MustCompile(`.*index\.html.*`, 0),
+ },
+ {
+ Name: "X-Baz",
+ Value: "baz",
+ Scope: regexp2.MustCompile(`.*another\.html.*`, 0),
+ },
}},
},
noDeadline: false,
@@ -1751,6 +1816,10 @@ func TestChromiumBrowser_screenshot(t *testing.T) {
expectError: false,
expectedLogEntries: []string{
"extra HTTP headers:",
+ "extra HTTP header 'X-Foo' will be set for request URL",
+ "extra HTTP header 'X-Bar' (scoped) will be set for request URL",
+ "extra HTTP header 'X-Baz' (scoped) will not be set for request URL",
+ "setting extra HTTP headers for request URL",
},
},
{
diff --git a/pkg/modules/chromium/chromium.go b/pkg/modules/chromium/chromium.go
index 735b2700..0a8a8a2e 100644
--- a/pkg/modules/chromium/chromium.go
+++ b/pkg/modules/chromium/chromium.go
@@ -9,6 +9,7 @@ import (
"github.com/alexliesenfeld/health"
"github.com/chromedp/cdproto/network"
+ "github.com/dlclark/regexp2"
flag "github.com/spf13/pflag"
"go.uber.org/zap"
@@ -109,7 +110,7 @@ type Options struct {
// ExtraHttpHeaders are extra HTTP headers to send by Chromium while
// loading he HTML document.
- ExtraHttpHeaders map[string]string
+ ExtraHttpHeaders []ExtraHttpHeader
// EmulatedMediaType is the media type to emulate, either "screen" or
// "print".
@@ -289,6 +290,22 @@ type Cookie struct {
SameSite network.CookieSameSite `json:"sameSite,omitempty"`
}
+// ExtraHttpHeader are extra HTTP headers to send by Chromium.
+type ExtraHttpHeader struct {
+ // Name is the header name.
+ // Required.
+ Name string
+
+ // Value is the header value.
+ // Required.
+ Value string
+
+ // Scope is the header scope. If nil, the header will be applied to ALL
+ // requests from the page.
+ // Optional.
+ Scope *regexp2.Regexp
+}
+
// Api helps to interact with Chromium for converting HTML documents to PDF.
type Api interface {
Pdf(ctx context.Context, logger *zap.Logger, url, outputPath string, options PdfOptions) error
diff --git a/pkg/modules/chromium/events.go b/pkg/modules/chromium/events.go
index 8a8ca1dd..85d71456 100644
--- a/pkg/modules/chromium/events.go
+++ b/pkg/modules/chromium/events.go
@@ -20,10 +20,22 @@ import (
"github.com/gotenberg/gotenberg/v8/pkg/gotenberg"
)
+type eventRequestPausedOptions struct {
+ allowList, denyList *regexp2.Regexp
+ extraHttpHeaders []ExtraHttpHeader
+}
+
// listenForEventRequestPaused listens for requests to check if they are
-// allowed or not.network.SetBlockedURLS()
+// allowed or not. It also set the extra HTTP headers, if any.
+// See https://github.com/gotenberg/gotenberg/issues/1011.
// TODO: https://chromedevtools.github.io/devtools-protocol/tot/Network/#method-setBlockedURLs (experimental for now).
-func listenForEventRequestPaused(ctx context.Context, logger *zap.Logger, allowList *regexp2.Regexp, denyList *regexp2.Regexp) {
+func listenForEventRequestPaused(ctx context.Context, logger *zap.Logger, options eventRequestPausedOptions) {
+ if len(options.extraHttpHeaders) == 0 {
+ logger.Debug("no extra HTTP headers")
+ } else {
+ logger.Debug(fmt.Sprintf("extra HTTP headers: %+v", options.extraHttpHeaders))
+ }
+
chromedp.ListenTarget(ctx, func(ev interface{}) {
switch e := ev.(type) {
case *fetch.EventRequestPaused:
@@ -37,7 +49,7 @@ func listenForEventRequestPaused(ctx context.Context, logger *zap.Logger, allowL
return
}
- err := gotenberg.FilterDeadline(allowList, denyList, e.Request.URL, deadline)
+ err := gotenberg.FilterDeadline(options.allowList, options.denyList, e.Request.URL, deadline)
if err != nil {
logger.Warn(err.Error())
allow = false
@@ -46,19 +58,78 @@ func listenForEventRequestPaused(ctx context.Context, logger *zap.Logger, allowL
cctx := chromedp.FromContext(ctx)
executorCtx := cdp.WithExecutor(ctx, cctx.Target)
- if allow {
- req := fetch.ContinueRequest(e.RequestID)
+ if !allow {
+ req := fetch.FailRequest(e.RequestID, network.ErrorReasonAccessDenied)
err = req.Do(executorCtx)
if err != nil {
- logger.Error(fmt.Sprintf("continue request: %s", err))
+ logger.Error(fmt.Sprintf("fail request: %s", err))
}
return
}
- req := fetch.FailRequest(e.RequestID, network.ErrorReasonAccessDenied)
+ req := fetch.ContinueRequest(e.RequestID)
+
+ var extraHttpHeadersToSet []ExtraHttpHeader
+ if len(options.extraHttpHeaders) > 0 {
+ // The user want to set extra HTTP headers.
+
+ // First, we have to check if at least one header has to be
+ // set for current request.
+ for _, header := range options.extraHttpHeaders {
+ if header.Scope == nil {
+ // Non-scoped header.
+ logger.Debug(fmt.Sprintf("extra HTTP header '%s' will be set for request URL '%s'", header.Name, e.Request.URL))
+ extraHttpHeadersToSet = append(extraHttpHeadersToSet, header)
+ continue
+ }
+
+ ok, err := header.Scope.MatchString(e.Request.URL)
+ if err != nil {
+ logger.Error(fmt.Sprintf("fail to match extra HTTP header '%s' scope with URL '%s': %s", header.Name, e.Request.URL, err))
+ } else if ok {
+ logger.Debug(fmt.Sprintf("extra HTTP header '%s' (scoped) will be set for request URL '%s'", header.Name, e.Request.URL))
+ extraHttpHeadersToSet = append(extraHttpHeadersToSet, header)
+ } else {
+ logger.Debug(fmt.Sprintf("scoped extra HTTP header '%s' (scoped) will not be set for request URL '%s'", header.Name, e.Request.URL))
+ }
+ }
+ }
+
+ if len(extraHttpHeadersToSet) > 0 {
+ logger.Debug(fmt.Sprintf("setting extra HTTP headers for request URL '%s': %+v", e.Request.URL, extraHttpHeadersToSet))
+
+ originalHeaders := e.Request.Headers
+ headers := make(map[string]string)
+
+ for key, value := range originalHeaders {
+ strValue, ok := value.(string)
+ if ok {
+ headers[key] = strValue
+ } else {
+ logger.Error(fmt.Sprintf("ignoring header '%s' for URL '%s' since it cannot be cast to a string", key, e.Request.URL))
+ }
+ }
+
+ var headersEntries []*fetch.HeaderEntry
+ for key, value := range headers {
+ headersEntries = append(headersEntries, &fetch.HeaderEntry{
+ Name: key,
+ Value: value,
+ })
+ }
+ for _, header := range extraHttpHeadersToSet {
+ headersEntries = append(headersEntries, &fetch.HeaderEntry{
+ Name: header.Name,
+ Value: header.Value,
+ })
+ }
+
+ req.Headers = headersEntries
+ }
+
err = req.Do(executorCtx)
if err != nil {
- logger.Error(fmt.Sprintf("fail request: %s", err))
+ logger.Error(fmt.Sprintf("continue request: %s", err))
}
}()
}
diff --git a/pkg/modules/chromium/routes.go b/pkg/modules/chromium/routes.go
index ba1e7be2..a725c7cd 100644
--- a/pkg/modules/chromium/routes.go
+++ b/pkg/modules/chromium/routes.go
@@ -13,6 +13,7 @@ import (
"strings"
"time"
+ "github.com/dlclark/regexp2"
"github.com/labstack/echo/v4"
"github.com/microcosm-cc/bluemonday"
"github.com/russross/blackfriday/v2"
@@ -36,7 +37,7 @@ func FormDataChromiumOptions(ctx *api.Context) (*api.FormData, Options) {
waitForExpression string
cookies []Cookie
userAgent string
- extraHttpHeaders map[string]string
+ extraHttpHeaders []ExtraHttpHeader
emulatedMediaType string
omitBackground bool
)
@@ -86,12 +87,59 @@ func FormDataChromiumOptions(ctx *api.Context) (*api.FormData, Options) {
return nil
}
- err := json.Unmarshal([]byte(value), &extraHttpHeaders)
+ var headers map[string]string
+ err := json.Unmarshal([]byte(value), &headers)
if err != nil {
return fmt.Errorf("unmarshal extraHttpHeaders: %w", err)
}
- return nil
+ for k, v := range headers {
+ var scope string
+ var valueTokens []string
+ var invalidScopeToken bool
+
+ tokens := strings.Split(v, ";")
+ for _, token := range tokens {
+ if strings.HasPrefix(strings.ToLower(strings.TrimSpace(token)), "scope") {
+ tokenNoSpaces := strings.Join(strings.Fields(token), "")
+ parts := strings.SplitN(tokenNoSpaces, "=", 2)
+
+ if len(parts) == 2 && strings.ToLower(parts[0]) == "scope" && parts[1] != "" {
+ scope = parts[1]
+ } else {
+ err = multierr.Append(err, fmt.Errorf("invalid scope '%s' for header '%s'", scope, k))
+ invalidScopeToken = true
+ break
+ }
+ } else {
+ if token != "" {
+ valueTokens = append(valueTokens, token)
+ }
+ }
+ }
+
+ if invalidScopeToken {
+ continue
+ }
+
+ var scopeRegexp *regexp2.Regexp
+ if len(scope) > 0 {
+ p, errCompile := regexp2.Compile(scope, 0)
+ if errCompile != nil {
+ err = multierr.Append(err, fmt.Errorf("invalid scope regex pattern for header '%s': %w", k, errCompile))
+ continue
+ }
+ scopeRegexp = p
+ }
+
+ extraHttpHeaders = append(extraHttpHeaders, ExtraHttpHeader{
+ Name: k,
+ Value: strings.Join(valueTokens, "; "),
+ Scope: scopeRegexp,
+ })
+ }
+
+ return err
}).
Custom("emulatedMediaType", func(value string) error {
if value == "" {
diff --git a/pkg/modules/chromium/routes_test.go b/pkg/modules/chromium/routes_test.go
index ba957e89..29fb9d10 100644
--- a/pkg/modules/chromium/routes_test.go
+++ b/pkg/modules/chromium/routes_test.go
@@ -7,8 +7,10 @@ import (
"net/http"
"os"
"reflect"
+ "sort"
"testing"
+ "github.com/dlclark/regexp2"
"github.com/google/uuid"
"github.com/labstack/echo/v4"
"go.uber.org/zap"
@@ -19,14 +21,18 @@ import (
func TestFormDataChromiumOptions(t *testing.T) {
for _, tc := range []struct {
- scenario string
- ctx *api.ContextMock
- expectedOptions Options
+ scenario string
+ ctx *api.ContextMock
+ expectedOptions Options
+ compareWithoutDeepEqual bool
+ expectValidationError bool
}{
{
- scenario: "no custom form fields",
- ctx: &api.ContextMock{Context: new(api.Context)},
- expectedOptions: DefaultOptions(),
+ scenario: "no custom form fields",
+ ctx: &api.ContextMock{Context: new(api.Context)},
+ expectedOptions: DefaultOptions(),
+ compareWithoutDeepEqual: false,
+ expectValidationError: false,
},
{
scenario: "invalid failOnHttpStatusCodes form field",
@@ -44,6 +50,8 @@ func TestFormDataChromiumOptions(t *testing.T) {
options.FailOnHttpStatusCodes = nil
return options
}(),
+ compareWithoutDeepEqual: false,
+ expectValidationError: true,
},
{
scenario: "valid failOnHttpStatusCodes form field",
@@ -61,6 +69,8 @@ func TestFormDataChromiumOptions(t *testing.T) {
options.FailOnHttpStatusCodes = []int64{399, 499, 599}
return options
}(),
+ compareWithoutDeepEqual: false,
+ expectValidationError: false,
},
{
scenario: "invalid cookies form field",
@@ -73,7 +83,9 @@ func TestFormDataChromiumOptions(t *testing.T) {
})
return ctx
}(),
- expectedOptions: DefaultOptions(),
+ expectedOptions: DefaultOptions(),
+ compareWithoutDeepEqual: false,
+ expectValidationError: true,
},
{
scenario: "invalid cookies form field (missing required values)",
@@ -93,6 +105,8 @@ func TestFormDataChromiumOptions(t *testing.T) {
options.Cookies = []Cookie{{}}
return options
}(),
+ compareWithoutDeepEqual: false,
+ expectValidationError: true,
},
{
scenario: "valid cookies form field",
@@ -114,9 +128,11 @@ func TestFormDataChromiumOptions(t *testing.T) {
}}
return options
}(),
+ compareWithoutDeepEqual: false,
+ expectValidationError: false,
},
{
- scenario: "invalid extraHttpHeaders form field",
+ scenario: "invalid extraHttpHeaders form field: cannot unmarshall",
ctx: func() *api.ContextMock {
ctx := &api.ContextMock{Context: new(api.Context)}
ctx.SetValues(map[string][]string{
@@ -126,7 +142,39 @@ func TestFormDataChromiumOptions(t *testing.T) {
})
return ctx
}(),
- expectedOptions: DefaultOptions(),
+ expectedOptions: DefaultOptions(),
+ compareWithoutDeepEqual: false,
+ expectValidationError: true,
+ },
+ {
+ scenario: "invalid extraHttpHeaders form field: invalid scope",
+ ctx: func() *api.ContextMock {
+ ctx := &api.ContextMock{Context: new(api.Context)}
+ ctx.SetValues(map[string][]string{
+ "extraHttpHeaders": {
+ `{"foo":"bar;scope;;"}`,
+ },
+ })
+ return ctx
+ }(),
+ expectedOptions: DefaultOptions(),
+ compareWithoutDeepEqual: false,
+ expectValidationError: true,
+ },
+ {
+ scenario: "invalid extraHttpHeaders form field: invalid scope regex pattern",
+ ctx: func() *api.ContextMock {
+ ctx := &api.ContextMock{Context: new(api.Context)}
+ ctx.SetValues(map[string][]string{
+ "extraHttpHeaders": {
+ `{"foo":"bar;scope=*."}`,
+ },
+ })
+ return ctx
+ }(),
+ expectedOptions: DefaultOptions(),
+ compareWithoutDeepEqual: false,
+ expectValidationError: true,
},
{
scenario: "valid extraHttpHeaders form field",
@@ -134,18 +182,28 @@ func TestFormDataChromiumOptions(t *testing.T) {
ctx := &api.ContextMock{Context: new(api.Context)}
ctx.SetValues(map[string][]string{
"extraHttpHeaders": {
- `{"foo":"bar"}`,
+ `{"foo":"bar","baz":"qux;scope=https?:\\/\\/([a-zA-Z0-9-]+\\.)*qux\\.com\\/.*"}`,
},
})
return ctx
}(),
expectedOptions: func() Options {
options := DefaultOptions()
- options.ExtraHttpHeaders = map[string]string{
- "foo": "bar",
+ options.ExtraHttpHeaders = []ExtraHttpHeader{
+ {
+ Name: "foo",
+ Value: "bar",
+ },
+ {
+ Name: "baz",
+ Value: "qux",
+ Scope: regexp2.MustCompile(`https?:\/\/([a-zA-Z0-9-]+\.)*qux\.com\/.*`, 0),
+ },
}
return options
}(),
+ compareWithoutDeepEqual: true,
+ expectValidationError: false,
},
{
scenario: "invalid emulatedMediaType form field",
@@ -158,7 +216,8 @@ func TestFormDataChromiumOptions(t *testing.T) {
})
return ctx
}(),
- expectedOptions: DefaultOptions(),
+ expectedOptions: DefaultOptions(),
+ expectValidationError: true,
},
{
scenario: "valid emulatedMediaType form field",
@@ -176,14 +235,61 @@ func TestFormDataChromiumOptions(t *testing.T) {
options.EmulatedMediaType = "screen"
return options
}(),
+ expectValidationError: false,
},
} {
t.Run(tc.scenario, func(t *testing.T) {
tc.ctx.SetLogger(zap.NewNop())
- _, actual := FormDataChromiumOptions(tc.ctx.Context)
+ form, actual := FormDataChromiumOptions(tc.ctx.Context)
- if !reflect.DeepEqual(actual, tc.expectedOptions) {
- t.Fatalf("expected %+v but got: %+v", tc.expectedOptions, actual)
+ if tc.compareWithoutDeepEqual {
+ if len(tc.expectedOptions.ExtraHttpHeaders) != len(actual.ExtraHttpHeaders) {
+ t.Fatalf("expected %d extra HTTP headers, but got %d", len(tc.expectedOptions.ExtraHttpHeaders), len(actual.ExtraHttpHeaders))
+ }
+
+ sort.Slice(tc.expectedOptions.ExtraHttpHeaders, func(i, j int) bool {
+ return tc.expectedOptions.ExtraHttpHeaders[i].Name < tc.expectedOptions.ExtraHttpHeaders[j].Name
+ })
+ sort.Slice(actual.ExtraHttpHeaders, func(i, j int) bool {
+ return actual.ExtraHttpHeaders[i].Name < actual.ExtraHttpHeaders[j].Name
+ })
+
+ for i := range tc.expectedOptions.ExtraHttpHeaders {
+ if tc.expectedOptions.ExtraHttpHeaders[i].Name != actual.ExtraHttpHeaders[i].Name {
+ t.Fatalf("expected '%s' extra HTTP header, but got '%s'", tc.expectedOptions.ExtraHttpHeaders[i].Name, tc.expectedOptions.ExtraHttpHeaders[i].Name)
+ }
+
+ if tc.expectedOptions.ExtraHttpHeaders[i].Value != actual.ExtraHttpHeaders[i].Value {
+ t.Fatalf("expected '%s' as value for extra HTTP header '%s', but got '%s'", tc.expectedOptions.ExtraHttpHeaders[i].Value, tc.expectedOptions.ExtraHttpHeaders[i].Name, actual.ExtraHttpHeaders[i].Value)
+ }
+
+ var expectedScope string
+ if tc.expectedOptions.ExtraHttpHeaders[i].Scope != nil {
+ expectedScope = tc.expectedOptions.ExtraHttpHeaders[i].Scope.String()
+ }
+ var actualScope string
+ if actual.ExtraHttpHeaders[i].Scope != nil {
+ actualScope = actual.ExtraHttpHeaders[i].Scope.String()
+ }
+
+ if expectedScope != actualScope {
+ t.Fatalf("expected '%s' as scope for extra HTTP header '%s', but got '%s'", expectedScope, tc.expectedOptions.ExtraHttpHeaders[i].Name, actualScope)
+ }
+ }
+ } else {
+ if !reflect.DeepEqual(actual, tc.expectedOptions) {
+ t.Fatalf("expected %+v but got: %+v", tc.expectedOptions, actual)
+ }
+ }
+
+ err := form.Validate()
+
+ if tc.expectValidationError && err == nil {
+ t.Fatal("expected validation error but got none", err)
+ }
+
+ if !tc.expectValidationError && err != nil {
+ t.Fatalf("expected no validation error but got: %v", err)
}
})
}
@@ -191,14 +297,16 @@ func TestFormDataChromiumOptions(t *testing.T) {
func TestFormDataChromiumPdfOptions(t *testing.T) {
for _, tc := range []struct {
- scenario string
- ctx *api.ContextMock
- expectedOptions PdfOptions
+ scenario string
+ ctx *api.ContextMock
+ expectedOptions PdfOptions
+ expectValidationError bool
}{
{
- scenario: "no custom form fields",
- ctx: &api.ContextMock{Context: new(api.Context)},
- expectedOptions: DefaultPdfOptions(),
+ scenario: "no custom form fields",
+ ctx: &api.ContextMock{Context: new(api.Context)},
+ expectedOptions: DefaultPdfOptions(),
+ expectValidationError: false,
},
{
scenario: "custom form fields (Options & PdfOptions)",
@@ -220,29 +328,42 @@ func TestFormDataChromiumPdfOptions(t *testing.T) {
options.EmulatedMediaType = "screen"
return options
}(),
+ expectValidationError: false,
},
} {
t.Run(tc.scenario, func(t *testing.T) {
tc.ctx.SetLogger(zap.NewNop())
- _, actual := FormDataChromiumPdfOptions(tc.ctx.Context)
+ form, actual := FormDataChromiumPdfOptions(tc.ctx.Context)
if !reflect.DeepEqual(actual, tc.expectedOptions) {
t.Fatalf("expected %+v but got: %+v", tc.expectedOptions, actual)
}
+
+ err := form.Validate()
+
+ if tc.expectValidationError && err == nil {
+ t.Fatal("expected validation error but got none", err)
+ }
+
+ if !tc.expectValidationError && err != nil {
+ t.Fatalf("expected no validation error but got: %v", err)
+ }
})
}
}
func TestFormDataChromiumScreenshotOptions(t *testing.T) {
for _, tc := range []struct {
- scenario string
- ctx *api.ContextMock
- expectedOptions ScreenshotOptions
+ scenario string
+ ctx *api.ContextMock
+ expectedOptions ScreenshotOptions
+ expectValidationError bool
}{
{
- scenario: "no custom form fields",
- ctx: &api.ContextMock{Context: new(api.Context)},
- expectedOptions: DefaultScreenshotOptions(),
+ scenario: "no custom form fields",
+ ctx: &api.ContextMock{Context: new(api.Context)},
+ expectedOptions: DefaultScreenshotOptions(),
+ expectValidationError: false,
},
{
scenario: "invalid format form field",
@@ -260,6 +381,7 @@ func TestFormDataChromiumScreenshotOptions(t *testing.T) {
options.Format = ""
return options
}(),
+ expectValidationError: true,
},
{
scenario: "valid png format form field",
@@ -277,6 +399,7 @@ func TestFormDataChromiumScreenshotOptions(t *testing.T) {
options.Format = "png"
return options
}(),
+ expectValidationError: false,
},
{
scenario: "valid jpeg format form field",
@@ -294,6 +417,7 @@ func TestFormDataChromiumScreenshotOptions(t *testing.T) {
options.Format = "jpeg"
return options
}(),
+ expectValidationError: false,
},
{
scenario: "valid webp format form field",
@@ -311,6 +435,7 @@ func TestFormDataChromiumScreenshotOptions(t *testing.T) {
options.Format = "webp"
return options
}(),
+ expectValidationError: false,
},
{
scenario: "invalid quality form field (not an integer)",
@@ -328,6 +453,7 @@ func TestFormDataChromiumScreenshotOptions(t *testing.T) {
options.Quality = 0
return options
}(),
+ expectValidationError: true,
},
{
scenario: "invalid quality form field (< 0)",
@@ -345,6 +471,7 @@ func TestFormDataChromiumScreenshotOptions(t *testing.T) {
options.Quality = 0
return options
}(),
+ expectValidationError: true,
},
{
scenario: "invalid quality form field (> 100)",
@@ -362,6 +489,7 @@ func TestFormDataChromiumScreenshotOptions(t *testing.T) {
options.Quality = 0
return options
}(),
+ expectValidationError: true,
},
{
scenario: "valid quality form field",
@@ -379,6 +507,7 @@ func TestFormDataChromiumScreenshotOptions(t *testing.T) {
options.Quality = 50
return options
}(),
+ expectValidationError: false,
},
{
scenario: "custom form fields (Options & ScreenshotOptions)",
@@ -412,29 +541,42 @@ func TestFormDataChromiumScreenshotOptions(t *testing.T) {
options.EmulatedMediaType = "screen"
return options
}(),
+ expectValidationError: false,
},
} {
t.Run(tc.scenario, func(t *testing.T) {
tc.ctx.SetLogger(zap.NewNop())
- _, actual := FormDataChromiumScreenshotOptions(tc.ctx.Context)
+ form, actual := FormDataChromiumScreenshotOptions(tc.ctx.Context)
if !reflect.DeepEqual(actual, tc.expectedOptions) {
t.Fatalf("expected %+v but got: %+v", tc.expectedOptions, actual)
}
+
+ err := form.Validate()
+
+ if tc.expectValidationError && err == nil {
+ t.Fatal("expected validation error but got none", err)
+ }
+
+ if !tc.expectValidationError && err != nil {
+ t.Fatalf("expected no validation error but got: %v", err)
+ }
})
}
}
func TestFormDataChromiumPdfFormats(t *testing.T) {
for _, tc := range []struct {
- scenario string
- ctx *api.ContextMock
- expectedPdfFormats gotenberg.PdfFormats
+ scenario string
+ ctx *api.ContextMock
+ expectedPdfFormats gotenberg.PdfFormats
+ expectValidationError bool
}{
{
- scenario: "no custom form fields",
- ctx: &api.ContextMock{Context: new(api.Context)},
- expectedPdfFormats: gotenberg.PdfFormats{},
+ scenario: "no custom form fields",
+ ctx: &api.ContextMock{Context: new(api.Context)},
+ expectedPdfFormats: gotenberg.PdfFormats{},
+ expectValidationError: false,
},
{
scenario: "pdfa and pdfua form fields",
@@ -450,30 +592,44 @@ func TestFormDataChromiumPdfFormats(t *testing.T) {
})
return ctx
}(),
- expectedPdfFormats: gotenberg.PdfFormats{PdfA: "foo", PdfUa: true},
+ expectedPdfFormats: gotenberg.PdfFormats{PdfA: "foo", PdfUa: true},
+ expectValidationError: false,
},
} {
t.Run(tc.scenario, func(t *testing.T) {
tc.ctx.SetLogger(zap.NewNop())
- actual := FormDataChromiumPdfFormats(tc.ctx.Context.FormData())
+ form := tc.ctx.Context.FormData()
+ actual := FormDataChromiumPdfFormats(form)
if !reflect.DeepEqual(actual, tc.expectedPdfFormats) {
t.Fatalf("expected %+v but got: %+v", tc.expectedPdfFormats, actual)
}
+
+ err := form.Validate()
+
+ if tc.expectValidationError && err == nil {
+ t.Fatal("expected validation error but got none", err)
+ }
+
+ if !tc.expectValidationError && err != nil {
+ t.Fatalf("expected no validation error but got: %v", err)
+ }
})
}
}
func TestFormDataPdfMetadata(t *testing.T) {
for _, tc := range []struct {
- scenario string
- ctx *api.ContextMock
- expectedMetadata map[string]interface{}
+ scenario string
+ ctx *api.ContextMock
+ expectedMetadata map[string]interface{}
+ expectValidationError bool
}{
{
- scenario: "no metadata form field",
- ctx: &api.ContextMock{Context: new(api.Context)},
- expectedMetadata: nil,
+ scenario: "no metadata form field",
+ ctx: &api.ContextMock{Context: new(api.Context)},
+ expectedMetadata: nil,
+ expectValidationError: false,
},
{
scenario: "invalid metadata form field",
@@ -486,7 +642,8 @@ func TestFormDataPdfMetadata(t *testing.T) {
})
return ctx
}(),
- expectedMetadata: nil,
+ expectedMetadata: nil,
+ expectValidationError: true,
},
{
scenario: "valid metadata form field",
@@ -502,15 +659,27 @@ func TestFormDataPdfMetadata(t *testing.T) {
expectedMetadata: map[string]interface{}{
"foo": "bar",
},
+ expectValidationError: false,
},
} {
t.Run(tc.scenario, func(t *testing.T) {
tc.ctx.SetLogger(zap.NewNop())
- actual := FormDataPdfMetadata(tc.ctx.Context.FormData())
+ form := tc.ctx.Context.FormData()
+ actual := FormDataPdfMetadata(form)
if !reflect.DeepEqual(actual, tc.expectedMetadata) {
t.Fatalf("expected %+v but got: %+v", tc.expectedMetadata, actual)
}
+
+ err := form.Validate()
+
+ if tc.expectValidationError && err == nil {
+ t.Fatal("expected validation error but got none", err)
+ }
+
+ if !tc.expectValidationError && err != nil {
+ t.Fatalf("expected no validation error but got: %v", err)
+ }
})
}
}
diff --git a/pkg/modules/chromium/tasks.go b/pkg/modules/chromium/tasks.go
index ac5d6e19..4e841d75 100644
--- a/pkg/modules/chromium/tasks.go
+++ b/pkg/modules/chromium/tasks.go
@@ -298,28 +298,33 @@ func userAgentOverride(logger *zap.Logger, userAgent string) chromedp.ActionFunc
}
}
-func extraHttpHeadersActionFunc(logger *zap.Logger, extraHttpHeaders map[string]string) chromedp.ActionFunc {
- return func(ctx context.Context) error {
- if len(extraHttpHeaders) == 0 {
- logger.Debug("no extra HTTP headers")
- return nil
- }
-
- logger.Debug(fmt.Sprintf("extra HTTP headers: %+v", extraHttpHeaders))
-
- headers := make(network.Headers, len(extraHttpHeaders))
- for key, value := range extraHttpHeaders {
- headers[key] = value
- }
-
- err := network.SetExtraHTTPHeaders(headers).Do(ctx)
- if err == nil {
- return nil
- }
-
- return fmt.Errorf("set extra HTTP headers: %w", err)
- }
-}
+// This code has been replaced with the listenForEventRequestPaused function.
+// Indeed, the user may want to scope the headers per domain, but using
+// network.SetExtraHTTPHeaders set the headers for ALL requests from the page.
+// See https://github.com/gotenberg/gotenberg/issues/1011.
+//
+//func extraHttpHeadersActionFunc(logger *zap.Logger, extraHttpHeaders map[string]string) chromedp.ActionFunc {
+// return func(ctx context.Context) error {
+// if len(extraHttpHeaders) == 0 {
+// logger.Debug("no extra HTTP headers")
+// return nil
+// }
+//
+// logger.Debug(fmt.Sprintf("extra HTTP headers: %+v", extraHttpHeaders))
+//
+// headers := make(network.Headers, len(extraHttpHeaders))
+// for key, value := range extraHttpHeaders {
+// headers[key] = value
+// }
+//
+// err := network.SetExtraHTTPHeaders(headers).Do(ctx)
+// if err == nil {
+// return nil
+// }
+//
+// return fmt.Errorf("set extra HTTP headers: %w", err)
+// }
+//}
func navigateActionFunc(logger *zap.Logger, url string, skipNetworkIdleEvent bool) chromedp.ActionFunc {
return func(ctx context.Context) error {