From 8e1ab0110f0010bcfce87df1c6b332c041b3b008 Mon Sep 17 00:00:00 2001 From: Julien Neuhart Date: Tue, 2 Jun 2026 19:05:34 +0200 Subject: [PATCH] refactor(chromium): classify Pdf and Screenshot errors and set span error.type --- pkg/modules/chromium/chromium.go | 57 +++++++++++++++----------- pkg/modules/chromium/errortype_test.go | 37 +++++++++++++++++ 2 files changed, 70 insertions(+), 24 deletions(-) create mode 100644 pkg/modules/chromium/errortype_test.go diff --git a/pkg/modules/chromium/chromium.go b/pkg/modules/chromium/chromium.go index eca485d5..5d9df45c 100644 --- a/pkg/modules/chromium/chromium.go +++ b/pkg/modules/chromium/chromium.go @@ -799,6 +799,8 @@ func (mod *Chromium) Routes() ([]api.Route, error) { } // Pdf converts a URL to PDF. +// +//nolint:dupl func (mod *Chromium) Pdf(ctx context.Context, logger *slog.Logger, url, outputPath string, options PdfOptions) error { ctx, span := gotenberg.Tracer().Start(ctx, "chromium.Pdf", trace.WithSpanKind(trace.SpanKindClient), @@ -824,21 +826,12 @@ func (mod *Chromium) Pdf(ctx context.Context, logger *slog.Logger, url, outputPa status = "error" } - reason := "unknown" - switch { - case errors.Is(err, context.DeadlineExceeded): - reason = "timeout" - case errors.Is(err, context.Canceled): - reason = "context_cancelled" - case errors.Is(err, ErrInvalidHttpStatusCode) || errors.Is(err, ErrInvalidResourceHttpStatusCode) || errors.Is(err, ErrLoadingFailed) || errors.Is(err, ErrResourceLoadingFailed) || errors.Is(err, ErrInvalidEvaluationExpression) || errors.Is(err, ErrInvalidSelectorQuery): - reason = "invalid_input" - case errors.Is(err, gotenberg.ErrMaximumQueueSizeExceeded) || errors.Is(err, gotenberg.ErrProcessAlreadyRestarting): - reason = "chromium_unavailable" - } + reason := chromiumErrorType(err, "chromium_unavailable") mod.errsCounter.Add(ctx, 1, metric.WithAttributes( attribute.String("reason", reason), )) + gotenberg.SpanErrorType(span, reason) } if !conversionStart.IsZero() { @@ -876,6 +869,9 @@ func (mod *Chromium) Pdf(ctx context.Context, logger *slog.Logger, url, outputPa return err } +// Screenshot captures a screenshot from a URL. +// +//nolint:dupl func (mod *Chromium) Screenshot(ctx context.Context, logger *slog.Logger, url, outputPath string, options ScreenshotOptions) error { ctx, span := gotenberg.Tracer().Start(ctx, "chromium.Screenshot", trace.WithSpanKind(trace.SpanKindClient), @@ -901,23 +897,12 @@ func (mod *Chromium) Screenshot(ctx context.Context, logger *slog.Logger, url, o status = "error" } - reason := "unknown" - switch { - case errors.Is(err, context.DeadlineExceeded): - reason = "timeout" - case errors.Is(err, context.Canceled): - reason = "context_cancelled" - case errors.Is(err, ErrInvalidHttpStatusCode) || errors.Is(err, ErrInvalidResourceHttpStatusCode) || errors.Is(err, ErrLoadingFailed) || errors.Is(err, ErrResourceLoadingFailed) || errors.Is(err, ErrInvalidEvaluationExpression) || errors.Is(err, ErrInvalidSelectorQuery): - reason = "invalid_input" - case errors.Is(err, gotenberg.ErrMaximumQueueSizeExceeded): - reason = "chromium_maximum_queue_size_exceeded" - case errors.Is(err, gotenberg.ErrProcessAlreadyRestarting): - reason = "chromium_unavailable" - } + reason := chromiumErrorType(err, "chromium_maximum_queue_size_exceeded") mod.errsCounter.Add(ctx, 1, metric.WithAttributes( attribute.String("reason", reason), )) + gotenberg.SpanErrorType(span, reason) } if !conversionStart.IsZero() { @@ -955,6 +940,30 @@ func (mod *Chromium) Screenshot(ctx context.Context, logger *slog.Logger, url, o return err } +// chromiumErrorType maps a conversion error to chromium's bounded reason value, +// reused as the span error.type. queueReason preserves the historical, +// route-specific label for a saturated queue: Pdf collapses it into +// "chromium_unavailable", whereas Screenshot keeps +// "chromium_maximum_queue_size_exceeded". Generic failures fall back to +// [gotenberg.ClassifyError]. +func chromiumErrorType(err error, queueReason string) string { + switch { + case errors.Is(err, ErrInvalidHttpStatusCode), + errors.Is(err, ErrInvalidResourceHttpStatusCode), + errors.Is(err, ErrLoadingFailed), + errors.Is(err, ErrResourceLoadingFailed), + errors.Is(err, ErrInvalidEvaluationExpression), + errors.Is(err, ErrInvalidSelectorQuery): + return gotenberg.ErrorTypeInvalidInput + case errors.Is(err, gotenberg.ErrMaximumQueueSizeExceeded): + return queueReason + case errors.Is(err, gotenberg.ErrProcessAlreadyRestarting): + return "chromium_unavailable" + default: + return gotenberg.ClassifyError(err) + } +} + // Interface guards. var ( _ gotenberg.Module = (*Chromium)(nil) diff --git a/pkg/modules/chromium/errortype_test.go b/pkg/modules/chromium/errortype_test.go new file mode 100644 index 00000000..113361d8 --- /dev/null +++ b/pkg/modules/chromium/errortype_test.go @@ -0,0 +1,37 @@ +package chromium + +import ( + "context" + "errors" + "testing" + + "github.com/gotenberg/gotenberg/v8/pkg/gotenberg" +) + +func TestChromiumErrorType(t *testing.T) { + for _, tc := range []struct { + name string + err error + queueReason string + want string + }{ + {"deadline", context.DeadlineExceeded, "chromium_unavailable", "timeout"}, + {"canceled", context.Canceled, "chromium_unavailable", "context_cancelled"}, + {"invalid http status", ErrInvalidHttpStatusCode, "chromium_unavailable", "invalid_input"}, + {"invalid resource http status", ErrInvalidResourceHttpStatusCode, "chromium_unavailable", "invalid_input"}, + {"loading failed", ErrLoadingFailed, "chromium_unavailable", "invalid_input"}, + {"resource loading failed", ErrResourceLoadingFailed, "chromium_unavailable", "invalid_input"}, + {"invalid evaluation expression", ErrInvalidEvaluationExpression, "chromium_unavailable", "invalid_input"}, + {"invalid selector query", ErrInvalidSelectorQuery, "chromium_unavailable", "invalid_input"}, + {"pdf queue", gotenberg.ErrMaximumQueueSizeExceeded, "chromium_unavailable", "chromium_unavailable"}, + {"screenshot queue", gotenberg.ErrMaximumQueueSizeExceeded, "chromium_maximum_queue_size_exceeded", "chromium_maximum_queue_size_exceeded"}, + {"restarting", gotenberg.ErrProcessAlreadyRestarting, "chromium_maximum_queue_size_exceeded", "chromium_unavailable"}, + {"unknown", errors.New("boom"), "chromium_unavailable", "unknown"}, + } { + t.Run(tc.name, func(t *testing.T) { + if got := chromiumErrorType(tc.err, tc.queueReason); got != tc.want { + t.Errorf("chromiumErrorType(%v, %q) = %q, want %q", tc.err, tc.queueReason, got, tc.want) + } + }) + } +}