diff --git a/pkg/modules/webhook/client.go b/pkg/modules/webhook/client.go index 7fefab01..ae053812 100644 --- a/pkg/modules/webhook/client.go +++ b/pkg/modules/webhook/client.go @@ -39,6 +39,11 @@ type client struct { func (c client) send(ctx context.Context, body io.Reader, headers map[string]string, errored bool) error { url := c.url if errored { + if c.errorUrl == "" { + // No error URL provided; error details will be sent + // via the events URL instead. + return nil + } url = c.errorUrl } diff --git a/pkg/modules/webhook/middleware.go b/pkg/modules/webhook/middleware.go index 482d10ed..c6cd7b5c 100644 --- a/pkg/modules/webhook/middleware.go +++ b/pkg/modules/webhook/middleware.go @@ -105,15 +105,22 @@ func webhookMiddleware(w *Webhook) api.Middleware { ctx := c.Get("context").(*api.Context) cancel := c.Get("cancel").(context.CancelFunc) - // Do we have a webhook error URL in case of... error? + // Do we have a webhook error URL and/or an events URL? + // At least one must be provided. webhookErrorUrl := c.Request().Header.Get("Gotenberg-Webhook-Error-Url") - if webhookErrorUrl == "" { + webhookEventsUrl := c.Request().Header.Get("Gotenberg-Webhook-Events-Url") + + if webhookErrorUrl == "" && webhookEventsUrl == "" { return api.WrapError( - errors.New("empty webhook error URL"), - api.NewSentinelHttpError(http.StatusBadRequest, "Invalid 'Gotenberg-Webhook-Error-Url' header: empty value or header not provided"), + errors.New("empty webhook error URL and events URL"), + api.NewSentinelHttpError(http.StatusBadRequest, "At least one of 'Gotenberg-Webhook-Error-Url' or 'Gotenberg-Webhook-Events-Url' headers must be provided"), ) } + if webhookErrorUrl != "" { + ctx.Log().Warn("'Gotenberg-Webhook-Error-Url' header is deprecated, use 'Gotenberg-Webhook-Events-Url' instead") + } + deadline, ok := ctx.Deadline() if !ok { return errors.New("context has no deadline") @@ -126,9 +133,11 @@ func webhookMiddleware(w *Webhook) api.Middleware { return fmt.Errorf("filter webhook URL: %w", err) } - err = gotenberg.FilterDeadline(w.errorAllowList, w.errorDenyList, webhookErrorUrl, deadline) - if err != nil { - return fmt.Errorf("filter webhook error URL: %w", err) + if webhookErrorUrl != "" { + err = gotenberg.FilterDeadline(w.errorAllowList, w.errorDenyList, webhookErrorUrl, deadline) + if err != nil { + return fmt.Errorf("filter webhook error URL: %w", err) + } } // Let's check the HTTP methods for calling the webhook URLs. @@ -164,9 +173,12 @@ func webhookMiddleware(w *Webhook) api.Middleware { return fmt.Errorf("get method to use for webhook: %w", err) } - webhookErrorMethod, err := methodFromHeader("Gotenberg-Webhook-Error-Method") - if err != nil { - return fmt.Errorf("get method to use for webhook error: %w", err) + var webhookErrorMethod string + if webhookErrorUrl != "" { + webhookErrorMethod, err = methodFromHeader("Gotenberg-Webhook-Error-Method") + if err != nil { + return fmt.Errorf("get method to use for webhook error: %w", err) + } } // What about extra HTTP headers? @@ -183,8 +195,7 @@ func webhookMiddleware(w *Webhook) api.Middleware { } } - // What about the events URL? - webhookEventsUrl := c.Request().Header.Get("Gotenberg-Webhook-Events-Url") + // Filter the events URL if provided. if webhookEventsUrl != "" { err = gotenberg.FilterDeadline(w.allowList, w.denyList, webhookEventsUrl, deadline) if err != nil { diff --git a/test/integration/features/webhook.feature b/test/integration/features/webhook.feature index 9229ce19..3800dd94 100644 --- a/test/integration/features/webhook.feature +++ b/test/integration/features/webhook.feature @@ -86,3 +86,43 @@ Feature: Webhook "timestamp": "ignore" } """ + + Scenario: Webhook Events URL Only (Success) + Given I have a default Gotenberg container + Given I have a webhook server + When I make a "POST" request to Gotenberg at the "/forms/pdfengines/flatten" endpoint with the following form data and header(s): + | files | testdata/page_1.pdf | file | + | Gotenberg-Webhook-Url | http://host.docker.internal:%d/webhook | header | + | Gotenberg-Webhook-Events-Url | http://host.docker.internal:%d/webhook/events | header | + Then the response status code should be 204 + When I wait for the asynchronous request to the webhook + Then the webhook request header "Content-Type" should be "application/pdf" + Then there should be 1 PDF(s) in the webhook request + Then the webhook event should match JSON: + """ + { + "event": "webhook.success", + "correlationId": "ignore", + "timestamp": "ignore" + } + """ + + Scenario: Webhook Events URL Only (Synchronous) + Given I have a Gotenberg container with the following environment variable(s): + | WEBHOOK_ENABLE_SYNC_MODE | true | + Given I have a webhook server + When I make a "POST" request to Gotenberg at the "/forms/pdfengines/flatten" endpoint with the following form data and header(s): + | files | testdata/page_1.pdf | file | + | Gotenberg-Webhook-Url | http://host.docker.internal:%d/webhook | header | + | Gotenberg-Webhook-Events-Url | http://host.docker.internal:%d/webhook/events | header | + Then the response status code should be 204 + Then the webhook request header "Content-Type" should be "application/pdf" + Then there should be 1 PDF(s) in the webhook request + Then the webhook event should match JSON: + """ + { + "event": "webhook.success", + "correlationId": "ignore", + "timestamp": "ignore" + } + """