mirror of
https://github.com/gotenberg/gotenberg.git
synced 2026-10-16 01:03:17 +01:00
Compare commits
7 Commits
v8.36.0
...
c2a85f92d0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c2a85f92d0 | ||
|
|
7dbff18e65 | ||
|
|
923e5f71eb | ||
|
|
334f859d95 | ||
|
|
0819514b7b | ||
|
|
c636a52666 | ||
|
|
0b16b0ab34 |
2
.github/workflows/continuous-integration.yml
vendored
2
.github/workflows/continuous-integration.yml
vendored
@@ -31,7 +31,7 @@ jobs:
|
||||
- name: Run linters
|
||||
uses: golangci/golangci-lint-action@v9
|
||||
with:
|
||||
version: v2.12.2
|
||||
version: v2.13.2
|
||||
|
||||
lint-prettier:
|
||||
name: Lint non-Golang codebase
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# ARG instructions do not create additional layers. Instead, next layers will
|
||||
# concatenate them. Also, we have to repeat ARG instructions in each build
|
||||
# stage that uses them.
|
||||
ARG GOLANG_VERSION=1.26.5
|
||||
ARG GOLANG_VERSION=1.27.0
|
||||
|
||||
# ----------------------------------------------
|
||||
# pdfcpu binary build stage
|
||||
@@ -88,7 +88,7 @@ RUN apt-get update -qq \
|
||||
|
||||
WORKDIR /downloads
|
||||
|
||||
RUN curl -Ls https://raw.githubusercontent.com/gotenberg/unoconverter/v0.4.0/unoconv -o unoconverter \
|
||||
RUN curl -Ls https://raw.githubusercontent.com/gotenberg/unoconverter/v0.5.0/unoconv -o unoconverter \
|
||||
&& chmod +x unoconverter
|
||||
|
||||
RUN curl -o pdftk-all.jar "https://gitlab.com/api/v4/projects/5024297/packages/generic/pdftk-java/$PDFTK_VERSION/pdftk-all.jar" \
|
||||
|
||||
15
go.mod
15
go.mod
@@ -1,11 +1,12 @@
|
||||
module github.com/gotenberg/gotenberg/v8
|
||||
|
||||
go 1.26.5
|
||||
go 1.27.0
|
||||
|
||||
require (
|
||||
github.com/alexliesenfeld/health v0.8.1
|
||||
github.com/chromedp/cdproto v0.0.0-20250803210736-d308e07a266d // pinned with chromedp v0.14.2, see below
|
||||
github.com/chromedp/chromedp v0.14.2 // pinned: v0.15.x breaks the headless print-mode paint pipeline (rAF / ResizeObserver / IntersectionObserver stop firing, blank charts). See https://github.com/gotenberg/gotenberg/issues/1535.
|
||||
github.com/coreos/go-oidc/v3 v3.20.0
|
||||
github.com/cucumber/godog v0.16.0
|
||||
github.com/dlclark/regexp2 v1.12.0
|
||||
github.com/gomarkdown/markdown v0.0.0-20260725000948-8435af3f5984
|
||||
@@ -20,10 +21,11 @@ require (
|
||||
github.com/prometheus/client_golang v1.24.1
|
||||
github.com/shirou/gopsutil/v4 v4.26.7
|
||||
github.com/spf13/pflag v1.0.10
|
||||
github.com/stretchr/testify v1.11.1
|
||||
github.com/stretchr/testify v1.12.1
|
||||
github.com/testcontainers/testcontainers-go v0.44.0
|
||||
go.opentelemetry.io/contrib/bridges/otelslog v0.20.0
|
||||
go.opentelemetry.io/contrib/exporters/autoexport v0.70.0
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.70.0
|
||||
go.opentelemetry.io/otel v1.45.0
|
||||
go.opentelemetry.io/otel/log v0.21.0
|
||||
go.opentelemetry.io/otel/metric v1.45.0
|
||||
@@ -37,11 +39,6 @@ require (
|
||||
golang.org/x/text v0.41.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/coreos/go-oidc/v3 v3.20.0
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.70.0
|
||||
)
|
||||
|
||||
require (
|
||||
dario.cat/mergo v1.0.2 // indirect
|
||||
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // indirect
|
||||
@@ -64,7 +61,6 @@ require (
|
||||
github.com/cpuguy83/dockercfg v0.3.2 // indirect
|
||||
github.com/cucumber/gherkin/go/v42 v42.0.1 // indirect
|
||||
github.com/cucumber/messages/go/v34 v34.2.1 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/distribution/reference v0.6.0 // indirect
|
||||
github.com/docker/go-connections v0.8.1 // indirect
|
||||
github.com/docker/go-units v0.5.0 // indirect
|
||||
@@ -106,7 +102,6 @@ require (
|
||||
github.com/opencontainers/go-digest v1.0.0 // indirect
|
||||
github.com/opencontainers/image-spec v1.1.1 // indirect
|
||||
github.com/pierrec/lz4/v4 v4.1.28 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/power-devops/perfstat v0.0.0-20260805114148-88456608a4f6 // indirect
|
||||
github.com/prometheus/client_model v0.6.2 // indirect
|
||||
github.com/prometheus/common v0.70.1 // indirect
|
||||
@@ -136,6 +131,7 @@ require (
|
||||
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.45.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.45.0 // indirect
|
||||
go.opentelemetry.io/proto/otlp v1.11.0 // indirect
|
||||
go.yaml.in/yaml/v3 v3.0.5 // indirect
|
||||
go4.org v0.0.0-20260112195520-a5071408f32f // indirect
|
||||
golang.org/x/crypto v0.55.0 // indirect
|
||||
golang.org/x/oauth2 v0.36.0 // indirect
|
||||
@@ -145,5 +141,4 @@ require (
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260810153831-ec0a7760b754 // indirect
|
||||
google.golang.org/grpc v1.83.0 // indirect
|
||||
google.golang.org/protobuf v1.36.12 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
||||
17
go.sum
17
go.sum
@@ -55,7 +55,6 @@ github.com/cucumber/godog v0.16.0/go.mod h1:EDUX9yCqANK+GpbftMDeu61sUDtdLuo1JJgX
|
||||
github.com/cucumber/messages/go/v34 v34.2.1 h1:qBPEl+HhNJuRX8Kjaw1Pm60KOODZf2/4WQAi0SR/neE=
|
||||
github.com/cucumber/messages/go/v34 v34.2.1/go.mod h1:LYUPjqlTS1kS0pdkdf6sS5uirnjwiIzEGyXPezXNhL8=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
|
||||
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
|
||||
@@ -130,10 +129,6 @@ github.com/klauspost/compress v1.19.2/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl
|
||||
github.com/klauspost/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek=
|
||||
github.com/klauspost/pgzip v1.2.6 h1:8RXeL5crjEUFnR2/Sn6GJNWtSQ3Dk8pq4CL3jvdDyjU=
|
||||
github.com/klauspost/pgzip v1.2.6/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
|
||||
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
|
||||
github.com/labstack/echo/v4 v4.15.4 h1:DL45vVYa+BWE+XuW+zZNd9H0YEdZ80UAWJGcTVW4EVs=
|
||||
@@ -192,7 +187,6 @@ github.com/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde h1:x0TT0RDC7UhA
|
||||
github.com/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde/go.mod h1:nZgzbfBr3hhjoZnS66nKrHmduYNpc34ny7RK4z5/HM0=
|
||||
github.com/pierrec/lz4/v4 v4.1.28 h1:pPEPwRJ4kybBTfGt28q7lQsRJQHhC08axprdLD5Ppio=
|
||||
github.com/pierrec/lz4/v4 v4.1.28/go.mod h1:EoQMVJgeeEOMsCqCzqFm2O0cJvljX2nGZjcRIPL34O4=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/power-devops/perfstat v0.0.0-20260805114148-88456608a4f6 h1:jL3a8soXdzuTCcRnKhOmtcsVOObdDTFf4O2B403HPRU=
|
||||
github.com/power-devops/perfstat v0.0.0-20260805114148-88456608a4f6/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
|
||||
@@ -206,8 +200,6 @@ github.com/prometheus/otlptranslator v1.0.0 h1:s0LJW/iN9dkIH+EnhiD3BlkkP5QVIUVEo
|
||||
github.com/prometheus/otlptranslator v1.0.0/go.mod h1:vRYWnXvI6aWGpsdY/mOT/cbeVRBlPWtBNDb7kGR3uKM=
|
||||
github.com/prometheus/procfs v0.21.1 h1:GljZCt+zSTS+NZq88cyQ1LjZ+RCHp3uVuabBWA5+OJI=
|
||||
github.com/prometheus/procfs v0.21.1/go.mod h1:aB55Cww9pdSJVHk0hUf0inxWyyjPogFIjmHKYgMKmtY=
|
||||
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
||||
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
||||
github.com/shirou/gopsutil/v4 v4.26.7 h1:IXzpHz/dkMRYAhKkOXr1HB6SuzWU3eoyyeWe7g3bNZc=
|
||||
github.com/shirou/gopsutil/v4 v4.26.7/go.mod h1:5O9FjBiXoTDFatIWjZZosqj4pV0DRtLx598xGbBehzM=
|
||||
github.com/sirupsen/logrus v1.10.0 h1:T8MxJJXVZkfcC5zSRMRAg2F8+lxjmUCGGWPzFxO+Msc=
|
||||
@@ -228,8 +220,8 @@ github.com/stretchr/objx v0.5.3/go.mod h1:rDQraq+vQZU7Fde9LOZLr8Tax6zZvy4kuNKF+Q
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
github.com/stretchr/testify v1.12.1 h1:EuwCh5fleGS7H32xRwO3wRGT7DxrDhLAT6FF8MpWDWE=
|
||||
github.com/stretchr/testify v1.12.1/go.mod h1:MDEgiDPPsNp5cuIrHPPCyornHKgEVbtFUmoNlxoYthg=
|
||||
github.com/testcontainers/testcontainers-go v0.44.0 h1:/Fwh6HY1mIikhnm9e7HwoxGycx0lzRAE0f5VQpjFxzI=
|
||||
github.com/testcontainers/testcontainers-go v0.44.0/go.mod h1:IcnwQrYTO86xHXu5bvMaBH7ATlbS3Qn1M1QWW3c66rE=
|
||||
github.com/tklauser/go-sysconf v0.4.0 h1:7H0uAN+7RkwWRaxhYXDLqa5V3LPrJeV8wmD9dRUgPQU=
|
||||
@@ -303,6 +295,8 @@ go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||
go.yaml.in/yaml/v2 v2.4.4 h1:tuyd0P+2Ont/d6e2rl3be67goVK4R6deVxCUX5vyPaQ=
|
||||
go.yaml.in/yaml/v2 v2.4.4/go.mod h1:gMZqIpDtDqOfM0uNfy0SkpRhvUryYH0Z6wdMYcacYXQ=
|
||||
go.yaml.in/yaml/v3 v3.0.5 h1:N6y/pJk8buWs9NY5ERU2HSMfm+IuD/OtfdAnq6kESPw=
|
||||
go.yaml.in/yaml/v3 v3.0.5/go.mod h1:HVTZu1O7/Vkt2N+BFy8Zza+lnLsABggaTM2ZpNIGuKg=
|
||||
go4.org v0.0.0-20260112195520-a5071408f32f h1:ziUVAjmTPwQMBmYR1tbdRFJPtTcQUI12fH9QQjfb0Sw=
|
||||
go4.org v0.0.0-20260112195520-a5071408f32f/go.mod h1:ZRJnO5ZI4zAwMFp+dS1+V6J6MSyAowhRqAE+DPa1Xp0=
|
||||
golang.org/x/crypto v0.55.0 h1:+KWHjbgOaAQ66dh/YlkZKHlz9ZUlq61AFirAR9ntP8M=
|
||||
@@ -338,10 +332,7 @@ google.golang.org/grpc v1.83.0/go.mod h1:kDyl6SKsiHKt0uylY5gtn5cEjkrIOhQOGDgIc4J
|
||||
google.golang.org/protobuf v1.36.12 h1:pJOKDDOyeXErUroCihFAd5LQuwXBSpVnKGrj5o/fwxc=
|
||||
google.golang.org/protobuf v1.36.12/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q=
|
||||
gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA=
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"os"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
|
||||
"go.opentelemetry.io/contrib/bridges/otelslog"
|
||||
@@ -74,6 +75,24 @@ func buildResource(ctx context.Context, logger *slog.Logger, serviceName, servic
|
||||
return merged
|
||||
}
|
||||
|
||||
// OTEL_*_EXPORTER select the exporter for each signal. autoexport treats an
|
||||
// unset or empty value as a request for the OTLP exporter, which then fails
|
||||
// against the default localhost:4318 endpoint when nothing listens there and,
|
||||
// for metrics, keeps retrying on the periodic reader's timer. Gotenberg keeps
|
||||
// telemetry opt-in: a signal with no exporter configured is built without one
|
||||
// and stays inert. See https://github.com/gotenberg/gotenberg/issues/1643.
|
||||
const (
|
||||
tracesExporterEnvKey = "OTEL_TRACES_EXPORTER"
|
||||
metricsExporterEnvKey = "OTEL_METRICS_EXPORTER"
|
||||
logsExporterEnvKey = "OTEL_LOGS_EXPORTER"
|
||||
)
|
||||
|
||||
// exporterConfigured reports whether the operator selected an exporter for the
|
||||
// signal owning envKey. An unset or blank value keeps that signal off.
|
||||
func exporterConfigured(envKey string) bool {
|
||||
return strings.TrimSpace(os.Getenv(envKey)) != ""
|
||||
}
|
||||
|
||||
// InitTracerProvider initializes the OpenTelemetry tracer provider.
|
||||
func InitTracerProvider(logger *slog.Logger, serviceName, serviceVersion string) (shutdown func(context.Context) error, err error) {
|
||||
initOtelLogger(logger)
|
||||
@@ -86,13 +105,14 @@ func InitTracerProvider(logger *slog.Logger, serviceName, serviceVersion string)
|
||||
trace.WithResource(res),
|
||||
}
|
||||
|
||||
traceExporter, err := autoexport.NewSpanExporter(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if !autoexport.IsNoneSpanExporter(traceExporter) {
|
||||
traceOpts = append(traceOpts, trace.WithBatcher(traceExporter))
|
||||
if exporterConfigured(tracesExporterEnvKey) {
|
||||
traceExporter, err := autoexport.NewSpanExporter(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !autoexport.IsNoneSpanExporter(traceExporter) {
|
||||
traceOpts = append(traceOpts, trace.WithBatcher(traceExporter))
|
||||
}
|
||||
}
|
||||
|
||||
traceProvider := trace.NewTracerProvider(traceOpts...)
|
||||
@@ -119,13 +139,14 @@ func InitMeterProvider(logger *slog.Logger, serviceName, serviceVersion string)
|
||||
}
|
||||
metricOpts = append(metricOpts, exemplarFilterOptions()...)
|
||||
|
||||
metricReader, err := autoexport.NewMetricReader(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if !autoexport.IsNoneMetricReader(metricReader) {
|
||||
metricOpts = append(metricOpts, metric.WithReader(metricReader))
|
||||
if exporterConfigured(metricsExporterEnvKey) {
|
||||
metricReader, err := autoexport.NewMetricReader(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !autoexport.IsNoneMetricReader(metricReader) {
|
||||
metricOpts = append(metricOpts, metric.WithReader(metricReader))
|
||||
}
|
||||
}
|
||||
|
||||
meterProvider := metric.NewMeterProvider(metricOpts...)
|
||||
@@ -157,13 +178,14 @@ func InitLoggerProvider(logger *slog.Logger, serviceName, serviceVersion string)
|
||||
log.WithResource(res),
|
||||
}
|
||||
|
||||
logExporter, err := autoexport.NewLogExporter(ctx)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
if !autoexport.IsNoneLogExporter(logExporter) {
|
||||
logOpts = append(logOpts, log.WithProcessor(log.NewBatchProcessor(logExporter)))
|
||||
if exporterConfigured(logsExporterEnvKey) {
|
||||
logExporter, err := autoexport.NewLogExporter(ctx)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
if !autoexport.IsNoneLogExporter(logExporter) {
|
||||
logOpts = append(logOpts, log.WithProcessor(log.NewBatchProcessor(logExporter)))
|
||||
}
|
||||
}
|
||||
|
||||
loggerProvider := log.NewLoggerProvider(logOpts...)
|
||||
|
||||
@@ -89,6 +89,49 @@ func TestInitTracerProvider_HonorsSamplerEnv(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestExporterConfigured pins the opt-in gate: an unset or blank
|
||||
// OTEL_*_EXPORTER keeps the signal off, so Gotenberg never wires the OTLP
|
||||
// exporter that autoexport would otherwise default to and fail to reach at
|
||||
// localhost:4318. See https://github.com/gotenberg/gotenberg/issues/1643.
|
||||
func TestExporterConfigured(t *testing.T) {
|
||||
const key = "OTEL_METRICS_EXPORTER"
|
||||
|
||||
orig, had := os.LookupEnv(key)
|
||||
t.Cleanup(func() {
|
||||
if had {
|
||||
os.Setenv(key, orig)
|
||||
return
|
||||
}
|
||||
os.Unsetenv(key)
|
||||
})
|
||||
|
||||
for _, tc := range []struct {
|
||||
scenario string
|
||||
unset bool
|
||||
value string
|
||||
want bool
|
||||
}{
|
||||
{"unset", true, "", false},
|
||||
{"empty", false, "", false},
|
||||
{"whitespace only", false, " ", false},
|
||||
{"none", false, "none", true},
|
||||
{"otlp", false, "otlp", true},
|
||||
{"padded value", false, " otlp ", true},
|
||||
} {
|
||||
t.Run(tc.scenario, func(t *testing.T) {
|
||||
if tc.unset {
|
||||
os.Unsetenv(key)
|
||||
} else {
|
||||
os.Setenv(key, tc.value)
|
||||
}
|
||||
|
||||
if got := exporterConfigured(key); got != tc.want {
|
||||
t.Errorf("exporterConfigured(%q) = %v, want %v", key, got, tc.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestExemplarFilterOptions(t *testing.T) {
|
||||
t.Run("default pins trace-based", func(t *testing.T) {
|
||||
if v, ok := os.LookupEnv("OTEL_METRICS_EXEMPLAR_FILTER"); ok {
|
||||
|
||||
@@ -612,7 +612,7 @@ func (form *FormData) mustValue(key string, target any, defaultValue any) *FormD
|
||||
val, ok := form.values[key]
|
||||
|
||||
if !ok || val[0] == "" {
|
||||
switch t := (target).(type) {
|
||||
switch t := target.(type) {
|
||||
case *string:
|
||||
*t = defaultValue.(string)
|
||||
case *bool:
|
||||
@@ -659,7 +659,7 @@ func (form *FormData) mustMandatoryField(key string, target any) *FormData {
|
||||
func (form *FormData) mustAssign(key, value string, target any) *FormData {
|
||||
var err error
|
||||
|
||||
switch t := (target).(type) {
|
||||
switch t := target.(type) {
|
||||
case *string:
|
||||
*t = value
|
||||
case *bool:
|
||||
|
||||
@@ -523,8 +523,45 @@ func (b *chromiumBrowser) do(ctx context.Context, logger *slog.Logger, url strin
|
||||
cancelOnMainPageError: taskCancel,
|
||||
})
|
||||
|
||||
var (
|
||||
crashed error
|
||||
crashedMu sync.RWMutex
|
||||
)
|
||||
|
||||
// See https://github.com/gotenberg/gotenberg/issues/1640.
|
||||
listenForEventTargetCrashed(taskCtx, logger, eventTargetCrashedOptions{
|
||||
crashed: &crashed,
|
||||
crashedMu: &crashedMu,
|
||||
cancel: taskCancel,
|
||||
})
|
||||
|
||||
runErr := chromedp.Run(taskCtx, tasks...)
|
||||
|
||||
// A crashed renderer is the root cause of every other failure this
|
||||
// conversion may have recorded, so check it first.
|
||||
// See https://github.com/gotenberg/gotenberg/issues/1640.
|
||||
crashedMu.RLock()
|
||||
defer crashedMu.RUnlock()
|
||||
|
||||
if crashed != nil {
|
||||
return fmt.Errorf("handle tasks: %w", crashed)
|
||||
}
|
||||
|
||||
// The browser context is only ever canceled when the browser process
|
||||
// dies or is stopped, never on a request timeout. If the run failed
|
||||
// and the browser context is done, the conversion failed because the
|
||||
// browser went away mid-flight; fail fast with the same crash error
|
||||
// instead of letting the error fall through as a generic context
|
||||
// cancellation. The check is gated on runErr so a successful
|
||||
// conversion is never discarded by a browser death that lands right
|
||||
// after it.
|
||||
// See https://github.com/gotenberg/gotenberg/issues/1640.
|
||||
if runErr != nil {
|
||||
if err := b.ctx.Err(); err != nil {
|
||||
return fmt.Errorf("handle tasks: %w", ErrChromiumCrashed)
|
||||
}
|
||||
}
|
||||
|
||||
// Check event-driven errors first — they take priority over chromedp.Run
|
||||
// errors because they carry the actual root cause (e.g., HTTP 500 from
|
||||
// the main page). When we cancel taskCtx on a main page error,
|
||||
|
||||
@@ -71,6 +71,10 @@ var (
|
||||
// ErrResourceLoadingFailed happens when one or more resources failed to load.
|
||||
ErrResourceLoadingFailed = errors.New("resource loading failed")
|
||||
|
||||
// ErrChromiumCrashed happens when the Chromium renderer crashes during a
|
||||
// conversion.
|
||||
ErrChromiumCrashed = errors.New("chromium crashed")
|
||||
|
||||
// PDF specific.
|
||||
|
||||
// ErrOmitBackgroundWithoutPrintBackground happens if
|
||||
@@ -1106,6 +1110,8 @@ func chromiumErrorType(err error, queueReason string) string {
|
||||
errors.Is(err, ErrInvalidEvaluationExpression),
|
||||
errors.Is(err, ErrInvalidSelectorQuery):
|
||||
return gotenberg.ErrorTypeInvalidInput
|
||||
case errors.Is(err, ErrChromiumCrashed):
|
||||
return "chromium_unavailable"
|
||||
case errors.Is(err, gotenberg.ErrMaximumQueueSizeExceeded):
|
||||
return queueReason
|
||||
case errors.Is(err, gotenberg.ErrProcessAlreadyRestarting):
|
||||
|
||||
@@ -21,6 +21,7 @@ func TestChromiumErrorType(t *testing.T) {
|
||||
{"invalid resource http status", ErrInvalidResourceHttpStatusCode, "chromium_unavailable", "invalid_input"},
|
||||
{"loading failed", ErrLoadingFailed, "chromium_unavailable", "invalid_input"},
|
||||
{"resource loading failed", ErrResourceLoadingFailed, "chromium_unavailable", "invalid_input"},
|
||||
{"crashed", ErrChromiumCrashed, "chromium_unavailable", "chromium_unavailable"},
|
||||
{"invalid evaluation expression", ErrInvalidEvaluationExpression, "chromium_unavailable", "invalid_input"},
|
||||
{"invalid selector query", ErrInvalidSelectorQuery, "chromium_unavailable", "invalid_input"},
|
||||
{"pdf queue", gotenberg.ErrMaximumQueueSizeExceeded, "chromium_unavailable", "chromium_unavailable"},
|
||||
|
||||
@@ -14,6 +14,7 @@ import (
|
||||
|
||||
"github.com/chromedp/cdproto/cdp"
|
||||
"github.com/chromedp/cdproto/fetch"
|
||||
"github.com/chromedp/cdproto/inspector"
|
||||
"github.com/chromedp/cdproto/network"
|
||||
"github.com/chromedp/cdproto/page"
|
||||
"github.com/chromedp/cdproto/runtime"
|
||||
@@ -521,6 +522,38 @@ func listenForEventExceptionThrown(ctx context.Context, logger *slog.Logger, con
|
||||
})
|
||||
}
|
||||
|
||||
type eventTargetCrashedOptions struct {
|
||||
crashed *error
|
||||
crashedMu *sync.RWMutex
|
||||
cancel context.CancelFunc
|
||||
}
|
||||
|
||||
// listenForEventTargetCrashed listens for the Inspector.targetCrashed event,
|
||||
// which Chromium sends when the renderer serving the conversion's tab
|
||||
// crashes. chromedp enables the Inspector domain on every target but does
|
||||
// not handle this event: left alone, the in-flight CDP command never
|
||||
// receives a response and the conversion blocks until the request deadline.
|
||||
// Record the crash and cancel the task context so the conversion fails fast
|
||||
// instead.
|
||||
// See https://github.com/gotenberg/gotenberg/issues/1640.
|
||||
func listenForEventTargetCrashed(ctx context.Context, logger *slog.Logger, options eventTargetCrashedOptions) {
|
||||
chromedp.ListenTarget(ctx, func(ev any) {
|
||||
if _, ok := ev.(*inspector.EventTargetCrashed); ok {
|
||||
logger.DebugContext(ctx, "event EventTargetCrashed fired")
|
||||
|
||||
options.crashedMu.Lock()
|
||||
defer options.crashedMu.Unlock()
|
||||
|
||||
*options.crashed = ErrChromiumCrashed
|
||||
|
||||
// Cancel the task context so the in-flight CDP command aborts
|
||||
// immediately instead of waiting for a response the crashed
|
||||
// renderer can never send.
|
||||
options.cancel()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// waitForEventDomContentEventFired registers a listener for the
|
||||
// DomContentEventFired event and returns a waiter that blocks until the
|
||||
// event fires or ctx is done. The listener registers at call time, not
|
||||
|
||||
@@ -1020,6 +1020,16 @@ func handleChromiumError(err error, options Options) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
if errors.Is(err, ErrChromiumCrashed) {
|
||||
return api.WrapError(
|
||||
err,
|
||||
api.NewSentinelHttpError(
|
||||
http.StatusServiceUnavailable,
|
||||
"Chromium crashed while processing the request. Retry, or reduce the workload if the problem persists.",
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
if errors.Is(err, ErrInvalidEvaluationExpression) {
|
||||
if options.WaitForExpression == "" {
|
||||
// We do not expect the 'waitWindowStatus' form field to return
|
||||
|
||||
51
pkg/modules/chromium/routes_test.go
Normal file
51
pkg/modules/chromium/routes_test.go
Normal file
@@ -0,0 +1,51 @@
|
||||
package chromium
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"github.com/gotenberg/gotenberg/v8/pkg/modules/api"
|
||||
)
|
||||
|
||||
// TestHandleChromiumError_Crashed pins the mapping of a Chromium renderer
|
||||
// crash to a 503 Service Unavailable. When the renderer crashes mid-conversion,
|
||||
// the request must fail fast with 503 rather than hang until the deadline and
|
||||
// surface as a generic timeout.
|
||||
// See https://github.com/gotenberg/gotenberg/issues/1640.
|
||||
func TestHandleChromiumError_Crashed(t *testing.T) {
|
||||
// Mirror the wrapping done by [chromiumBrowser.do].
|
||||
err := handleChromiumError(fmt.Errorf("handle tasks: %w", ErrChromiumCrashed), Options{})
|
||||
if err == nil {
|
||||
t.Fatal("expected an error, got none")
|
||||
}
|
||||
|
||||
status, message := api.ParseError(err)
|
||||
if status != http.StatusServiceUnavailable {
|
||||
t.Errorf("status = %d, want %d (message: %s)", status, http.StatusServiceUnavailable, message)
|
||||
}
|
||||
|
||||
want := "Chromium crashed while processing the request. Retry, or reduce the workload if the problem persists."
|
||||
if message != want {
|
||||
t.Errorf("message = %q, want %q", message, want)
|
||||
}
|
||||
}
|
||||
|
||||
// TestHandleChromiumError_CrashedTakesPrecedence guards the ordering in
|
||||
// [handleChromiumError]: a crash is a server-side failure and must map to 503
|
||||
// even when the error chain also carries a marker that another branch would
|
||||
// map to a client-error status.
|
||||
func TestHandleChromiumError_CrashedTakesPrecedence(t *testing.T) {
|
||||
err := handleChromiumError(
|
||||
fmt.Errorf("handle tasks: %w; %w", ErrChromiumCrashed, ErrInvalidHttpStatusCode),
|
||||
Options{},
|
||||
)
|
||||
if err == nil {
|
||||
t.Fatal("expected an error, got none")
|
||||
}
|
||||
|
||||
status, _ := api.ParseError(err)
|
||||
if status != http.StatusServiceUnavailable {
|
||||
t.Errorf("status = %d, want %d", status, http.StatusServiceUnavailable)
|
||||
}
|
||||
}
|
||||
@@ -1401,3 +1401,19 @@ Feature: /forms/chromium/convert/url
|
||||
Then the response header "Content-Type" should be "application/pdf"
|
||||
Then there should be 1 PDF(s) in the response
|
||||
Then the "foo.pdf" PDF should have 1 page(s)
|
||||
|
||||
# chrome://crash makes the renderer crash deterministically, the same
|
||||
# failure class as a renderer crash triggered by the page content. The
|
||||
# request must fail fast with a 503 instead of hanging until the API
|
||||
# timeout.
|
||||
# See https://github.com/gotenberg/gotenberg/issues/1640.
|
||||
Scenario: POST /forms/chromium/convert/url (Chromium crash fails fast with 503)
|
||||
Given I have a default Gotenberg container
|
||||
When I make a "POST" request to Gotenberg at the "/forms/chromium/convert/url" endpoint with the following form data and header(s):
|
||||
| url | chrome://crash | field |
|
||||
Then the response status code should be 503
|
||||
Then the response header "Content-Type" should be "text/plain; charset=UTF-8"
|
||||
Then the response body should contain string:
|
||||
"""
|
||||
Chromium crashed while processing the request
|
||||
"""
|
||||
|
||||
Reference in New Issue
Block a user