From 9aaab97d6acc254739a6c6b774988a8ef1990797 Mon Sep 17 00:00:00 2001 From: Julien Neuhart Date: Thu, 15 May 2025 09:22:52 +0200 Subject: [PATCH] fix(pdfengines): read metadata better webhook error - fixes #1148 --- pkg/modules/pdfengines/routes.go | 5 +++++ pkg/modules/webhook/middleware.go | 17 +++++++++++------ .../features/pdfengines_metadata.feature | 18 ++++++++++++++++++ test/integration/scenario/scenario.go | 18 +++--------------- test/integration/scenario/server.go | 14 +++++++++++--- 5 files changed, 48 insertions(+), 24 deletions(-) diff --git a/pkg/modules/pdfengines/routes.go b/pkg/modules/pdfengines/routes.go index ac800a23..a3161424 100644 --- a/pkg/modules/pdfengines/routes.go +++ b/pkg/modules/pdfengines/routes.go @@ -500,6 +500,11 @@ func readMetadataRoute(engine gotenberg.PdfEngine) api.Route { err = c.JSON(http.StatusOK, res) if err != nil { + if strings.Contains(err.Error(), "request method or response status code does not allow body") { + // High probability that the user is using the webhook + // feature. It does not make sense for this route. + return api.ErrNoOutputFile + } return fmt.Errorf("return JSON response: %w", err) } diff --git a/pkg/modules/webhook/middleware.go b/pkg/modules/webhook/middleware.go index ec2904f2..4360d344 100644 --- a/pkg/modules/webhook/middleware.go +++ b/pkg/modules/webhook/middleware.go @@ -184,11 +184,21 @@ func webhookMiddleware(w *Webhook) api.Middleware { // Call the next middleware in the chain. err := next(c) if err != nil { + if errors.Is(err, api.ErrNoOutputFile) { + errNoOutputFile := fmt.Errorf("%w - the webhook middleware cannot handle the result of this route", err) + handleAsyncError(api.WrapError( + errNoOutputFile, + api.NewSentinelHttpError( + http.StatusBadRequest, + "The webhook middleware can only work with multipart/form-data routes that results in output files", + ), + )) + return + } // The process failed for whatever reason. Let's send the // details to the webhook. ctx.Log().Error(err.Error()) handleAsyncError(err) - return } @@ -197,7 +207,6 @@ func webhookMiddleware(w *Webhook) api.Middleware { if err != nil { ctx.Log().Error(fmt.Sprintf("build output file: %s", err)) handleAsyncError(err) - return } @@ -205,7 +214,6 @@ func webhookMiddleware(w *Webhook) api.Middleware { if err != nil { ctx.Log().Error(fmt.Sprintf("open output file: %s", err)) handleAsyncError(err) - return } @@ -221,7 +229,6 @@ func webhookMiddleware(w *Webhook) api.Middleware { if err != nil { ctx.Log().Error(fmt.Sprintf("read header of output file: %s", err)) handleAsyncError(err) - return } @@ -229,7 +236,6 @@ func webhookMiddleware(w *Webhook) api.Middleware { if err != nil { ctx.Log().Error(fmt.Sprintf("get stat from output file: %s", err)) handleAsyncError(err) - return } @@ -237,7 +243,6 @@ func webhookMiddleware(w *Webhook) api.Middleware { if err != nil { ctx.Log().Error(fmt.Sprintf("reset output file reader: %s", err)) handleAsyncError(err) - return } diff --git a/test/integration/features/pdfengines_metadata.feature b/test/integration/features/pdfengines_metadata.feature index 1cbb0ee1..19dd0b8f 100644 --- a/test/integration/features/pdfengines_metadata.feature +++ b/test/integration/features/pdfengines_metadata.feature @@ -233,6 +233,24 @@ Feature: /forms/pdfengines/{write|read} } """ + Scenario: POST /forms/pdfengines/metadata/read (Webhook) + 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/metadata/read" 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-Error-Url | http://host.docker.internal:%d/webhook/error | 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/json" + Then the webhook request body should match JSON: + """ + { + "status": 400, + "message": "the webhook middleware can only work with multipart/form-data routes that results in output files" + } + """ + Scenario: POST /forms/pdfengines/metadata/write (Basic Auth) Given I have a Gotenberg container with the following environment variable(s): | API_ENABLE_BASIC_AUTH | true | diff --git a/test/integration/scenario/scenario.go b/test/integration/scenario/scenario.go index b76e739d..a8f31a93 100644 --- a/test/integration/scenario/scenario.go +++ b/test/integration/scenario/scenario.go @@ -407,11 +407,7 @@ func (s *scenario) theBodyShouldMatchString(kind string, expectedDoc *godog.DocS } else if s.server.req == nil { return errors.New("no webhook request found") } else { - body, err := io.ReadAll(s.server.req.Body) - if err != nil { - return fmt.Errorf("read request body: %w", err) - } - actual = string(body) + actual = string(s.server.bodyCopy) } expected := strings.ReplaceAll(expectedDoc.Content, "{version}", GotenbergVersion) @@ -431,11 +427,7 @@ func (s *scenario) theBodyShouldContainString(kind string, expectedDoc *godog.Do } else if s.server.req == nil { return errors.New("no webhook request found") } else { - body, err := io.ReadAll(s.server.req.Body) - if err != nil { - return fmt.Errorf("read request body: %w", err) - } - actual = string(body) + actual = string(s.server.bodyCopy) } expected := strings.ReplaceAll(expectedDoc.Content, "{version}", GotenbergVersion) @@ -455,11 +447,7 @@ func (s *scenario) theBodyShouldMatchJSON(kind string, expectedDoc *godog.DocStr } else if s.server.req == nil { return errors.New("no webhook request found") } else { - b, err := io.ReadAll(s.server.req.Body) - if err != nil { - return fmt.Errorf("read request body: %w", err) - } - body = b + body = s.server.bodyCopy } var expected, actual interface{} diff --git a/test/integration/scenario/server.go b/test/integration/scenario/server.go index d4912113..861d70fb 100644 --- a/test/integration/scenario/server.go +++ b/test/integration/scenario/server.go @@ -19,9 +19,10 @@ import ( ) type server struct { - srv *echo.Echo - req *http.Request - errChan chan error + srv *echo.Echo + req *http.Request + bodyCopy []byte + errChan chan error } func newServer(ctx context.Context, workdir string) (*server, error) { @@ -51,6 +52,8 @@ func newServer(ctx context.Context, workdir string) (*server, error) { return webhookErr(fmt.Errorf("read request body: %w", err)) } + s.bodyCopy = body + cd := s.req.Header.Get("Content-Disposition") if cd == "" { return webhookErr(fmt.Errorf("no Content-Disposition header")) @@ -125,6 +128,11 @@ func newServer(ctx context.Context, workdir string) (*server, error) { } webhookErrorHandler := func(c echo.Context) error { s.req = c.Request() + body, err := io.ReadAll(s.req.Body) + if err != nil { + return webhookErr(fmt.Errorf("read request body: %w", err)) + } + s.bodyCopy = body return webhookErr(c.String(http.StatusOK, http.StatusText(http.StatusOK))) }