From b1f3d6d4c01d1c21535e129548f7f4c6fdca0c89 Mon Sep 17 00:00:00 2001 From: Julien Neuhart Date: Sun, 18 Feb 2024 13:05:48 +0100 Subject: [PATCH] feat: handling more ErrMaximumQueueSizeExceeded scenarios --- Makefile | 4 + pkg/gotenberg/supervisor.go | 2 +- pkg/modules/chromium/chromium.go | 2 +- pkg/modules/chromium/routes.go | 32 +++--- pkg/modules/chromium/routes_test.go | 40 ++++++++ pkg/modules/libreoffice/routes.go | 34 ++++++- pkg/modules/libreoffice/routes_test.go | 132 +++++++++++++++++++++++++ pkg/modules/pdfengines/routes.go | 30 ++++++ pkg/modules/pdfengines/routes_test.go | 77 ++++++++++++++- 9 files changed, 336 insertions(+), 17 deletions(-) diff --git a/Makefile b/Makefile index aecd5ef0..21b316c7 100644 --- a/Makefile +++ b/Makefile @@ -35,6 +35,7 @@ API_ROOT_PATH=/ API_TRACE_HEADER=Gotenberg-Trace API_DISABLE_HEALTH_CHECK_LOGGING=false CHROMIUM_RESTART_AFTER=0 +CHROMIUM_MAX_QUEUE_SIZE=0 CHROMIUM_AUTO_START=false CHROMIUM_START_TIMEOUT=20s CHROMIUM_INCOGNITO=false @@ -51,6 +52,7 @@ CHROMIUM_CLEAR_COOKIES=false CHROMIUM_DISABLE_JAVASCRIPT=false CHROMIUM_DISABLE_ROUTES=false LIBREOFFICE_RESTART_AFTER=10 +LIBREOFFICE_MAX_QUEUE_SIZE=0 LIBREOFFICE_AUTO_START=false LIBREOFFICE_START_TIMEOUT=20s LIBREOFFICE_DISABLE_ROUTES=false @@ -89,6 +91,7 @@ run: ## Start a Gotenberg container --api-disable-health-check-logging=$(API_DISABLE_HEALTH_CHECK_LOGGING) \ --chromium-restart-after=$(CHROMIUM_RESTART_AFTER) \ --chromium-auto-start=$(CHROMIUM_AUTO_START) \ + --chromium-max-queue-size=$(CHROMIUM_MAX_QUEUE_SIZE) \ --chromium-start-timeout=$(CHROMIUM_START_TIMEOUT) \ --chromium-incognito=$(CHROMIUM_INCOGNITO) \ --chromium-allow-insecure-localhost=$(CHROMIUM_ALLOW_INSECURE_LOCALHOST) \ @@ -104,6 +107,7 @@ run: ## Start a Gotenberg container --chromium-disable-javascript=$(CHROMIUM_DISABLE_JAVASCRIPT) \ --chromium-disable-routes=$(CHROMIUM_DISABLE_ROUTES) \ --libreoffice-restart-after=$(LIBREOFFICE_RESTART_AFTER) \ + --libreoffice-max-queue-size=$(LIBREOFFICE_MAX_QUEUE_SIZE) \ --libreoffice-auto-start=$(LIBREOFFICE_AUTO_START) \ --libreoffice-start-timeout=$(LIBREOFFICE_START_TIMEOUT) \ --libreoffice-disable-routes=$(LIBREOFFICE_DISABLE_ROUTES) \ diff --git a/pkg/gotenberg/supervisor.go b/pkg/gotenberg/supervisor.go index 1df1e554..408ca5ac 100644 --- a/pkg/gotenberg/supervisor.go +++ b/pkg/gotenberg/supervisor.go @@ -88,7 +88,7 @@ type processSupervisor struct { } // NewProcessSupervisor initializes a new [ProcessSupervisor]. -func NewProcessSupervisor(logger *zap.Logger, process Process, maxReqLimit int64, maxQueueSize int64) ProcessSupervisor { +func NewProcessSupervisor(logger *zap.Logger, process Process, maxReqLimit, maxQueueSize int64) ProcessSupervisor { b := &processSupervisor{ logger: logger, process: process, diff --git a/pkg/modules/chromium/chromium.go b/pkg/modules/chromium/chromium.go index beb37aa8..4e73410b 100644 --- a/pkg/modules/chromium/chromium.go +++ b/pkg/modules/chromium/chromium.go @@ -280,6 +280,7 @@ func (mod *Chromium) Descriptor() gotenberg.ModuleDescriptor { FlagSet: func() *flag.FlagSet { fs := flag.NewFlagSet("chromium", flag.ExitOnError) fs.Int64("chromium-restart-after", 0, "Number of conversions after which Chromium will automatically restart. Set to 0 to disable this feature") + fs.Int64("chromium-max-queue-size", 0, "Maximum request queue size for chromium. Set to 0 to disable this feature") fs.Bool("chromium-auto-start", false, "Automatically launch Chromium upon initialization if set to true; otherwise, Chromium will start at the time of the first conversion") fs.Duration("chromium-start-timeout", time.Duration(20)*time.Second, "Maximum duration to wait for Chromium to start or restart") fs.Bool("chromium-incognito", false, "Start Chromium with incognito mode") @@ -295,7 +296,6 @@ func (mod *Chromium) Descriptor() gotenberg.ModuleDescriptor { fs.Bool("chromium-clear-cookies", false, "Clear Chromium cookies between each conversion") fs.Bool("chromium-disable-javascript", false, "Disable JavaScript") fs.Bool("chromium-disable-routes", false, "Disable the routes") - fs.Int64("chromium-max-queue-size", 0, "Maximum request queue size for chromium. Set to 0 to disable this feature") return fs }(), diff --git a/pkg/modules/chromium/routes.go b/pkg/modules/chromium/routes.go index 5f1ab92d..e9d09344 100644 --- a/pkg/modules/chromium/routes.go +++ b/pkg/modules/chromium/routes.go @@ -512,7 +512,7 @@ func convertUrl(ctx *api.Context, chromium Api, engine gotenberg.PdfEngine, url if err != nil { if errors.Is(err, ErrOmitBackgroundWithoutPrintBackground) { return api.WrapError( - err, + fmt.Errorf("convert to PDF: %w", err), api.NewSentinelHttpError( http.StatusBadRequest, "omitBackground requires printBackground set to true", @@ -540,16 +540,6 @@ func convertUrl(ctx *api.Context, chromium Api, engine gotenberg.PdfEngine, url ) } - if errors.Is(err, gotenberg.ErrMaximumQueueSizeExceeded) { - return api.WrapError( - fmt.Errorf("convert to PDF: %w", err), - api.NewSentinelHttpError( - http.StatusTooManyRequests, - "The maximum queue size has been reached", - ), - ) - } - return fmt.Errorf("convert to PDF: %w", err) } @@ -563,6 +553,16 @@ func convertUrl(ctx *api.Context, chromium Api, engine gotenberg.PdfEngine, url err = engine.Convert(ctx, ctx.Log(), pdfFormats, convertInputPath, convertOutputPath) if err != nil { + if errors.Is(err, gotenberg.ErrMaximumQueueSizeExceeded) { + return api.WrapError( + fmt.Errorf("convert PDF: %w", err), + api.NewSentinelHttpError( + http.StatusTooManyRequests, + "The maximum queue size has been reached", + ), + ) + } + if errors.Is(err, gotenberg.ErrPdfFormatNotSupported) { return api.WrapError( fmt.Errorf("convert PDF: %w", err), @@ -611,6 +611,16 @@ func handleChromiumError(err error, url string, options Options) error { return nil } + if errors.Is(err, gotenberg.ErrMaximumQueueSizeExceeded) { + return api.WrapError( + err, + api.NewSentinelHttpError( + http.StatusTooManyRequests, + "The maximum queue size has been reached", + ), + ) + } + if errors.Is(err, ErrUrlNotAuthorized) { return api.WrapError( err, diff --git a/pkg/modules/chromium/routes_test.go b/pkg/modules/chromium/routes_test.go index b46440c1..bb5d7fe1 100644 --- a/pkg/modules/chromium/routes_test.go +++ b/pkg/modules/chromium/routes_test.go @@ -1230,6 +1230,18 @@ func TestConvertUrl(t *testing.T) { expectHttpStatus int expectOutputPathsCount int }{ + { + scenario: "ErrMaximumQueueSizeExceeded", + ctx: &api.ContextMock{Context: new(api.Context)}, + api: &ApiMock{PdfMock: func(ctx context.Context, logger *zap.Logger, url, outputPath string, options PdfOptions) error { + return gotenberg.ErrMaximumQueueSizeExceeded + }}, + options: DefaultPdfOptions(), + expectError: true, + expectHttpError: true, + expectHttpStatus: http.StatusTooManyRequests, + expectOutputPathsCount: 0, + }, { scenario: "ErrUrlNotAuthorized", ctx: &api.ContextMock{Context: new(api.Context)}, @@ -1341,6 +1353,22 @@ func TestConvertUrl(t *testing.T) { expectHttpError: false, expectOutputPathsCount: 0, }, + { + scenario: "ErrMaximumQueueSizeExceeded (PDF engine)", + ctx: &api.ContextMock{Context: new(api.Context)}, + api: &ApiMock{PdfMock: func(ctx context.Context, logger *zap.Logger, url, outputPath string, options PdfOptions) error { + return nil + }}, + engine: &gotenberg.PdfEngineMock{ConvertMock: func(ctx context.Context, logger *zap.Logger, formats gotenberg.PdfFormats, inputPath, outputPath string) error { + return gotenberg.ErrMaximumQueueSizeExceeded + }}, + pdfFormats: gotenberg.PdfFormats{PdfA: "foo"}, + options: DefaultPdfOptions(), + expectError: true, + expectHttpError: true, + expectHttpStatus: http.StatusTooManyRequests, + expectOutputPathsCount: 0, + }, { scenario: "ErrPdfFormatNotSupported", ctx: &api.ContextMock{Context: new(api.Context)}, @@ -1462,6 +1490,18 @@ func TestScreenshotUrl(t *testing.T) { expectHttpStatus int expectOutputPathsCount int }{ + { + scenario: "ErrMaximumQueueSizeExceeded", + ctx: &api.ContextMock{Context: new(api.Context)}, + api: &ApiMock{ScreenshotMock: func(ctx context.Context, logger *zap.Logger, url, outputPath string, options ScreenshotOptions) error { + return gotenberg.ErrMaximumQueueSizeExceeded + }}, + options: DefaultScreenshotOptions(), + expectError: true, + expectHttpError: true, + expectHttpStatus: http.StatusTooManyRequests, + expectOutputPathsCount: 0, + }, { scenario: "ErrUrlNotAuthorized", ctx: &api.ContextMock{Context: new(api.Context)}, diff --git a/pkg/modules/libreoffice/routes.go b/pkg/modules/libreoffice/routes.go index 95e352e7..eec44354 100644 --- a/pkg/modules/libreoffice/routes.go +++ b/pkg/modules/libreoffice/routes.go @@ -107,11 +107,21 @@ func convertRoute(libreOffice libreofficeapi.Uno, engine gotenberg.PdfEngine) ap err = engine.Merge(ctx, ctx.Log(), outputPaths, outputPath) if err != nil { + if errors.Is(err, gotenberg.ErrMaximumQueueSizeExceeded) { + return api.WrapError( + fmt.Errorf("merge PDFs: %w", err), + api.NewSentinelHttpError( + http.StatusTooManyRequests, + "The maximum queue size has been reached", + ), + ) + } + return fmt.Errorf("merge PDFs: %w", err) } - // Now, let's check if the client want to convert this result - // PDF to specific PDF formats. + // Now, let's check if the client want to convert this + // resulting PDF to specific PDF formats. zeroValued := gotenberg.PdfFormats{} if !nativePdfFormats && pdfFormats != zeroValued { convertInputPath := outputPath @@ -119,6 +129,16 @@ func convertRoute(libreOffice libreofficeapi.Uno, engine gotenberg.PdfEngine) ap err = engine.Convert(ctx, ctx.Log(), pdfFormats, convertInputPath, convertOutputPath) if err != nil { + if errors.Is(err, gotenberg.ErrMaximumQueueSizeExceeded) { + return api.WrapError( + fmt.Errorf("convert PDF: %w", err), + api.NewSentinelHttpError( + http.StatusTooManyRequests, + "The maximum queue size has been reached", + ), + ) + } + if errors.Is(err, gotenberg.ErrPdfFormatNotSupported) { return api.WrapError( fmt.Errorf("convert PDF: %w", err), @@ -160,6 +180,16 @@ func convertRoute(libreOffice libreofficeapi.Uno, engine gotenberg.PdfEngine) ap err = engine.Convert(ctx, ctx.Log(), pdfFormats, convertInputPath, convertOutputPaths[i]) if err != nil { + if errors.Is(err, gotenberg.ErrMaximumQueueSizeExceeded) { + return api.WrapError( + fmt.Errorf("convert PDF: %w", err), + api.NewSentinelHttpError( + http.StatusTooManyRequests, + "The maximum queue size has been reached", + ), + ) + } + if errors.Is(err, gotenberg.ErrPdfFormatNotSupported) { return api.WrapError( fmt.Errorf("convert PDF: %w", err), diff --git a/pkg/modules/libreoffice/routes_test.go b/pkg/modules/libreoffice/routes_test.go index d11eef94..7206febd 100644 --- a/pkg/modules/libreoffice/routes_test.go +++ b/pkg/modules/libreoffice/routes_test.go @@ -39,6 +39,28 @@ func TestConvertRoute(t *testing.T) { expectHttpStatus: http.StatusBadRequest, expectOutputPathsCount: 0, }, + { + scenario: "ErrMaximumQueueSizeExceeded", + ctx: func() *api.ContextMock { + ctx := &api.ContextMock{Context: new(api.Context)} + ctx.SetFiles(map[string]string{ + "document.docx": "/document.docx", + }) + return ctx + }(), + libreOffice: &libreofficeapi.ApiMock{ + PdfMock: func(ctx context.Context, logger *zap.Logger, inputPath, outputPath string, options libreofficeapi.Options) error { + return gotenberg.ErrMaximumQueueSizeExceeded + }, + ExtensionsMock: func() []string { + return []string{".docx"} + }, + }, + expectError: true, + expectHttpError: true, + expectHttpStatus: http.StatusTooManyRequests, + expectOutputPathsCount: 0, + }, { scenario: "ErrPdfFormatNotSupported (nativePdfFormats)", ctx: func() *api.ContextMock { @@ -109,6 +131,41 @@ func TestConvertRoute(t *testing.T) { expectHttpError: false, expectOutputPathsCount: 0, }, + { + scenario: "ErrMaximumQueueSizeExceeded (single file)", + ctx: func() *api.ContextMock { + ctx := &api.ContextMock{Context: new(api.Context)} + ctx.SetFiles(map[string]string{ + "document.docx": "/document.docx", + }) + ctx.SetValues(map[string][]string{ + "pdfa": { + gotenberg.PdfA1b, + }, + "nativePdfFormats": { + "false", + }, + }) + return ctx + }(), + libreOffice: &libreofficeapi.ApiMock{ + PdfMock: func(ctx context.Context, logger *zap.Logger, inputPath, outputPath string, options libreofficeapi.Options) error { + return nil + }, + ExtensionsMock: func() []string { + return []string{".docx"} + }, + }, + engine: &gotenberg.PdfEngineMock{ + ConvertMock: func(ctx context.Context, logger *zap.Logger, formats gotenberg.PdfFormats, inputPath, outputPath string) error { + return gotenberg.ErrMaximumQueueSizeExceeded + }, + }, + expectError: true, + expectHttpError: true, + expectHttpStatus: http.StatusTooManyRequests, + expectOutputPathsCount: 0, + }, { scenario: "ErrPdfFormatNotSupported (single file)", ctx: func() *api.ContextMock { @@ -321,6 +378,39 @@ func TestConvertRoute(t *testing.T) { expectOutputPathsCount: 2, expectOutpoutPaths: []string{"/document.docx.pdf", "/document2.docx.pdf"}, }, + { + scenario: "ErrMaximumQueueSizeExceeded (merge)", + ctx: func() *api.ContextMock { + ctx := &api.ContextMock{Context: new(api.Context)} + ctx.SetFiles(map[string]string{ + "document.docx": "/document.docx", + "document2.docx": "/document2.docx", + }) + ctx.SetValues(map[string][]string{ + "merge": { + "true", + }, + }) + return ctx + }(), + libreOffice: &libreofficeapi.ApiMock{ + PdfMock: func(ctx context.Context, logger *zap.Logger, inputPath, outputPath string, options libreofficeapi.Options) error { + return nil + }, + ExtensionsMock: func() []string { + return []string{".docx"} + }, + }, + engine: &gotenberg.PdfEngineMock{ + MergeMock: func(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error { + return gotenberg.ErrMaximumQueueSizeExceeded + }, + }, + expectError: true, + expectHttpError: true, + expectHttpStatus: http.StatusTooManyRequests, + expectOutputPathsCount: 0, + }, { scenario: "merge error", ctx: func() *api.ContextMock { @@ -353,6 +443,48 @@ func TestConvertRoute(t *testing.T) { expectHttpError: false, expectOutputPathsCount: 0, }, + { + scenario: "ErrMaximumQueueSizeExceeded (convert post-merge)", + ctx: func() *api.ContextMock { + ctx := &api.ContextMock{Context: new(api.Context)} + ctx.SetFiles(map[string]string{ + "document.docx": "/document.docx", + "document2.docx": "/document2.docx", + }) + ctx.SetValues(map[string][]string{ + "merge": { + "true", + }, + "pdfa": { + gotenberg.PdfA1b, + }, + "nativePdfFormats": { + "false", + }, + }) + return ctx + }(), + libreOffice: &libreofficeapi.ApiMock{ + PdfMock: func(ctx context.Context, logger *zap.Logger, inputPath, outputPath string, options libreofficeapi.Options) error { + return nil + }, + ExtensionsMock: func() []string { + return []string{".docx"} + }, + }, + engine: &gotenberg.PdfEngineMock{ + MergeMock: func(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error { + return nil + }, + ConvertMock: func(ctx context.Context, logger *zap.Logger, formats gotenberg.PdfFormats, inputPath, outputPath string) error { + return gotenberg.ErrMaximumQueueSizeExceeded + }, + }, + expectError: true, + expectHttpError: true, + expectHttpStatus: http.StatusTooManyRequests, + expectOutputPathsCount: 0, + }, { scenario: "ErrPdfFormatNotSupported (merge)", ctx: func() *api.ContextMock { diff --git a/pkg/modules/pdfengines/routes.go b/pkg/modules/pdfengines/routes.go index df41116f..0e7cd011 100644 --- a/pkg/modules/pdfengines/routes.go +++ b/pkg/modules/pdfengines/routes.go @@ -47,6 +47,16 @@ func mergeRoute(engine gotenberg.PdfEngine) api.Route { err = engine.Merge(ctx, ctx.Log(), inputPaths, outputPath) if err != nil { + if errors.Is(err, gotenberg.ErrMaximumQueueSizeExceeded) { + return api.WrapError( + fmt.Errorf("merge PDFs: %w", err), + api.NewSentinelHttpError( + http.StatusTooManyRequests, + "The maximum queue size has been reached", + ), + ) + } + return fmt.Errorf("merge PDFs: %w", err) } @@ -60,6 +70,16 @@ func mergeRoute(engine gotenberg.PdfEngine) api.Route { err = engine.Convert(ctx, ctx.Log(), pdfFormats, convertInputPath, convertOutputPath) if err != nil { + if errors.Is(err, gotenberg.ErrMaximumQueueSizeExceeded) { + return api.WrapError( + fmt.Errorf("convert PDF: %w", err), + api.NewSentinelHttpError( + http.StatusTooManyRequests, + "The maximum queue size has been reached", + ), + ) + } + if errors.Is(err, gotenberg.ErrPdfFormatNotSupported) { return api.WrapError( fmt.Errorf("convert PDF: %w", err), @@ -140,6 +160,16 @@ func convertRoute(engine gotenberg.PdfEngine) api.Route { err = engine.Convert(ctx, ctx.Log(), pdfFormats, inputPath, outputPaths[i]) if err != nil { + if errors.Is(err, gotenberg.ErrMaximumQueueSizeExceeded) { + return api.WrapError( + fmt.Errorf("convert PDF: %w", err), + api.NewSentinelHttpError( + http.StatusTooManyRequests, + "The maximum queue size has been reached", + ), + ) + } + if errors.Is(err, gotenberg.ErrPdfFormatNotSupported) { return api.WrapError( fmt.Errorf("convert PDF: %w", err), diff --git a/pkg/modules/pdfengines/routes_test.go b/pkg/modules/pdfengines/routes_test.go index 04c65d1f..9d7757cd 100644 --- a/pkg/modules/pdfengines/routes_test.go +++ b/pkg/modules/pdfengines/routes_test.go @@ -31,6 +31,27 @@ func TestMergeHandler(t *testing.T) { expectHttpStatus: http.StatusBadRequest, expectOutputPathsCount: 0, }, + { + scenario: "ErrMaximumQueueSizeExceeded (merge)", + ctx: func() *api.ContextMock { + ctx := &api.ContextMock{Context: new(api.Context)} + ctx.SetFiles(map[string]string{ + "file.pdf": "/file.pdf", + "file2.pdf": "/file2.pdf", + }) + + return ctx + }(), + engine: &gotenberg.PdfEngineMock{ + MergeMock: func(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error { + return gotenberg.ErrMaximumQueueSizeExceeded + }, + }, + expectError: true, + expectHttpError: true, + expectHttpStatus: http.StatusTooManyRequests, + expectOutputPathsCount: 0, + }, { scenario: "error from PDF engine", ctx: func() *api.ContextMock { @@ -90,7 +111,7 @@ func TestMergeHandler(t *testing.T) { expectOutputPathsCount: 1, }, { - scenario: "ErrPdfFormatNotSupported", + scenario: "ErrMaximumQueueSizeExceeded (convert)", ctx: func() *api.ContextMock { ctx := &api.ContextMock{Context: new(api.Context)} ctx.SetFiles(map[string]string{ @@ -104,6 +125,34 @@ func TestMergeHandler(t *testing.T) { }) return ctx }(), + engine: &gotenberg.PdfEngineMock{ + MergeMock: func(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error { + return nil + }, + ConvertMock: func(ctx context.Context, logger *zap.Logger, formats gotenberg.PdfFormats, inputPath, outputPath string) error { + return gotenberg.ErrMaximumQueueSizeExceeded + }, + }, + expectError: true, + expectHttpError: true, + expectHttpStatus: http.StatusTooManyRequests, + expectOutputPathsCount: 0, + }, + { + scenario: "ErrPdfFormatNotSupported", + ctx: func() *api.ContextMock { + ctx := &api.ContextMock{Context: new(api.Context)} + ctx.SetFiles(map[string]string{ + "file.pdf": "/file.pdf", + "file2.pdf": "/file2.pdf", + }) + ctx.SetValues(map[string][]string{ + "pdfa": { + "foo", + }, + }) + return ctx + }(), engine: &gotenberg.PdfEngineMock{ MergeMock: func(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error { return nil @@ -248,7 +297,7 @@ func TestConvertHandler(t *testing.T) { expectOutputPathsCount: 0, }, { - scenario: "ErrPdfFormatNotSupported", + scenario: "ErrMaximumQueueSizeExceeded", ctx: func() *api.ContextMock { ctx := &api.ContextMock{Context: new(api.Context)} ctx.SetFiles(map[string]string{ @@ -261,6 +310,30 @@ func TestConvertHandler(t *testing.T) { }) return ctx }(), + engine: &gotenberg.PdfEngineMock{ + ConvertMock: func(ctx context.Context, logger *zap.Logger, formats gotenberg.PdfFormats, inputPath, outputPath string) error { + return gotenberg.ErrMaximumQueueSizeExceeded + }, + }, + expectError: true, + expectHttpError: true, + expectHttpStatus: http.StatusTooManyRequests, + expectOutputPathsCount: 0, + }, + { + scenario: "ErrPdfFormatNotSupported", + ctx: func() *api.ContextMock { + ctx := &api.ContextMock{Context: new(api.Context)} + ctx.SetFiles(map[string]string{ + "file.pdf": "/file.pdf", + }) + ctx.SetValues(map[string][]string{ + "pdfa": { + "foo", + }, + }) + return ctx + }(), engine: &gotenberg.PdfEngineMock{ ConvertMock: func(ctx context.Context, logger *zap.Logger, formats gotenberg.PdfFormats, inputPath, outputPath string) error { return gotenberg.ErrPdfFormatNotSupported