diff --git a/.github/workflows/validate.yaml b/.github/workflows/validate.yaml index dcc04732e..dd20daf56 100644 --- a/.github/workflows/validate.yaml +++ b/.github/workflows/validate.yaml @@ -7,7 +7,7 @@ on: env: GO_VERSION: '1.23' - GOLANGCI_LINT_VERSION: v1.63.3 + GOLANGCI_LINT_VERSION: v1.64.2 MISSPELL_VERSION: v0.6.0 jobs: diff --git a/.golangci.yml b/.golangci.yml index c6e6b3b37..5b232fbcf 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -1,5 +1,6 @@ run: timeout: 10m + relative-path-mode: cfg linters-settings: govet: @@ -165,6 +166,7 @@ linters-settings: linters: enable-all: true disable: + - tenv # Deprecated - sqlclosecheck # not relevant (SQL) - rowserrcheck # not relevant (SQL) - cyclop # duplicate of gocyclo @@ -201,7 +203,6 @@ linters: - maintidx # kind of duplicate of gocyclo - nonamedreturns # Too strict - gosmopolitan # not relevant - - exportloopref # Not relevant since go1.22 issues: exclude-use-default: false diff --git a/CHANGELOG.md b/CHANGELOG.md index 2ac4ebbaa..91717cb86 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,12 @@ +# [v2.11.21](https://github.com/traefik/traefik/tree/v2.11.21) (2025-02-24) +[All Commits](https://github.com/traefik/traefik/compare/v2.11.20...v2.11.21) + +**Bug fixes:** +- **[acme]** Bump github.com/go-acme/lego/v4 to v4.22.2 ([#11537](https://github.com/traefik/traefik/pull/11537) by [ldez](https://github.com/ldez)) +- **[cli]** Bump github.com/traefik/paerser to v0.2.2 ([#11530](https://github.com/traefik/traefik/pull/11530) by [kevinpollet](https://github.com/kevinpollet)) +- **[middleware]** Enable the retry middleware in the proxy ([#11536](https://github.com/traefik/traefik/pull/11536) by [kevinpollet](https://github.com/kevinpollet)) +- **[middleware]** Retry should send headers on Write ([#11534](https://github.com/traefik/traefik/pull/11534) by [kevinpollet](https://github.com/kevinpollet)) + ## [v3.3.3](https://github.com/traefik/traefik/tree/v3.3.3) (2025-01-31) [All Commits](https://github.com/traefik/traefik/compare/v3.3.2...v3.3.3) diff --git a/docs/content/https/acme.md b/docs/content/https/acme.md index d7ebeb893..62f091e08 100644 --- a/docs/content/https/acme.md +++ b/docs/content/https/acme.md @@ -316,7 +316,7 @@ For complete details, refer to your provider's _Additional configuration_ link. | Provider Name | Provider Code | Environment Variables | | |------------------------------------------------------------------------|--------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------| -| [ACME DNS](https://github.com/joohoi/acme-dns) | `acme-dns` | `ACME_DNS_API_BASE`, `ACME_DNS_STORAGE_PATH` | [Additional configuration](https://go-acme.github.io/lego/dns/acme-dns) | +| [ACME DNS](https://github.com/joohoi/acme-dns) | `acme-dns` | `ACME_DNS_API_BASE`, `ACME_DNS_STORAGE_PATH`, `ACME_DNS_STORAGE_BASE_URL` | [Additional configuration](https://go-acme.github.io/lego/dns/acme-dns) | | [Alibaba Cloud](https://www.alibabacloud.com) | `alidns` | `ALICLOUD_ACCESS_KEY`, `ALICLOUD_SECRET_KEY`, `ALICLOUD_REGION_ID` | [Additional configuration](https://go-acme.github.io/lego/dns/alidns) | | [all-inkl](https://all-inkl.com) | `allinkl` | `ALL_INKL_LOGIN`, `ALL_INKL_PASSWORD` | [Additional configuration](https://go-acme.github.io/lego/dns/allinkl) | | [ArvanCloud](https://www.arvancloud.ir/en) | `arvancloud` | `ARVANCLOUD_API_KEY` | [Additional configuration](https://go-acme.github.io/lego/dns/arvancloud) | @@ -397,6 +397,7 @@ For complete details, refer to your provider's _Additional configuration_ link. | [Metaname](https://metaname.net) | `metaname` | `METANAME_ACCOUNT_REFERENCE`, `METANAME_API_KEY` | [Additional configuration](https://go-acme.github.io/lego/dns/metaname) | | [mijn.host](https://mijn.host/) | `mijnhost` | `MIJNHOST_API_KEY` | [Additional configuration](https://go-acme.github.io/lego/dns/mijnhost) | | [Mittwald](https://www.mittwald.de) | `mittwald` | `MITTWALD_TOKEN` | [Additional configuration](https://go-acme.github.io/lego/dns/mittwald) | +| [myaddr.{tools,dev,io}](https://myaddr.tools/) | `myaddr` | `MYADDR_PRIVATE_KEYS_MAPPING` | [Additional configuration](https://go-acme.github.io/lego/dns/myaddr) | | [MyDNS.jp](https://www.mydns.jp/) | `mydnsjp` | `MYDNSJP_MASTER_ID`, `MYDNSJP_PASSWORD` | [Additional configuration](https://go-acme.github.io/lego/dns/mydnsjp) | | [Mythic Beasts](https://www.mythic-beasts.com) | `mythicbeasts` | `MYTHICBEASTS_USER_NAME`, `MYTHICBEASTS_PASSWORD` | [Additional configuration](https://go-acme.github.io/lego/dns/mythicbeasts) | | [name.com](https://www.name.com/) | `namedotcom` | `NAMECOM_USERNAME`, `NAMECOM_API_TOKEN`, `NAMECOM_SERVER` | [Additional configuration](https://go-acme.github.io/lego/dns/namedotcom) | @@ -434,6 +435,7 @@ For complete details, refer to your provider's _Additional configuration_ link. | [Shellrent](https://www.shellrent.com) | `shellrent` | `SHELLRENT_USERNAME`, `SHELLRENT_TOKEN` | [Additional configuration](https://go-acme.github.io/lego/dns/shellrent) | | [Simply.com](https://www.simply.com/en/domains/) | `simply` | `SIMPLY_ACCOUNT_NAME`, `SIMPLY_API_KEY` | [Additional configuration](https://go-acme.github.io/lego/dns/simply) | | [Sonic](https://www.sonic.com/) | `sonic` | `SONIC_USER_ID`, `SONIC_API_KEY` | [Additional configuration](https://go-acme.github.io/lego/dns/sonic) | +| [Spaceship](https://spaceship.com) | `spaceship` | `SPACESHIP_API_KEY`, `SPACESHIP_API_SECRET` | [Additional configuration](https://go-acme.github.io/lego/dns/spaceship) | | [Stackpath](https://www.stackpath.com/) | `stackpath` | `STACKPATH_CLIENT_ID`, `STACKPATH_CLIENT_SECRET`, `STACKPATH_STACK_ID` | [Additional configuration](https://go-acme.github.io/lego/dns/stackpath) | | [Technitium](https://technitium.com) | `technitium` | `TECHNITIUM_SERVER_BASE_URL`, `TECHNITIUM_API_TOKEN` | [Additional configuration](https://go-acme.github.io/lego/dns/technitium) | | [Tencent Cloud DNS](https://cloud.tencent.com/product/cns) | `tencentcloud` | `TENCENTCLOUD_SECRET_ID`, `TENCENTCLOUD_SECRET_KEY` | [Additional configuration](https://go-acme.github.io/lego/dns/tencentcloud) | diff --git a/go.mod b/go.mod index 03cbb7a75..352b5dabb 100644 --- a/go.mod +++ b/go.mod @@ -17,7 +17,7 @@ require ( github.com/docker/go-connections v0.5.0 github.com/fatih/structs v1.1.0 github.com/fsnotify/fsnotify v1.8.0 - github.com/go-acme/lego/v4 v4.21.0 + github.com/go-acme/lego/v4 v4.22.2 github.com/go-kit/kit v0.13.0 github.com/go-kit/log v0.2.1 github.com/golang/protobuf v1.5.4 @@ -63,7 +63,7 @@ require ( github.com/tetratelabs/wazero v1.8.0 github.com/tidwall/gjson v1.17.0 github.com/traefik/grpc-web v0.16.0 - github.com/traefik/paerser v0.2.1 + github.com/traefik/paerser v0.2.2 github.com/traefik/yaegi v0.16.1 github.com/unrolled/render v1.0.2 github.com/unrolled/secure v1.0.9 @@ -87,7 +87,6 @@ require ( go.opentelemetry.io/otel/sdk/log v0.8.0 go.opentelemetry.io/otel/sdk/metric v1.28.0 go.opentelemetry.io/otel/trace v1.32.0 - golang.org/x/exp v0.0.0-20241210194714-1829a127f884 // No tag on the repo. golang.org/x/mod v0.22.0 golang.org/x/net v0.33.0 golang.org/x/sync v0.10.0 @@ -169,7 +168,6 @@ require ( github.com/containerd/log v0.1.0 // indirect github.com/containerd/platforms v0.2.1 // indirect github.com/coreos/go-semver v0.3.1 // indirect - github.com/cpu/goacmedns v0.1.1 // indirect github.com/cpuguy83/dockercfg v0.3.1 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/deepmap/oapi-codegen v1.9.1 // indirect @@ -281,6 +279,7 @@ require ( github.com/nrdcg/desec v0.10.0 // indirect github.com/nrdcg/dnspod-go v0.4.0 // indirect github.com/nrdcg/freemyip v0.3.0 // indirect + github.com/nrdcg/goacmedns v0.2.0 // indirect github.com/nrdcg/goinwx v0.10.0 // indirect github.com/nrdcg/mailinabox v0.2.0 // indirect github.com/nrdcg/namesilo v0.2.1 // indirect @@ -361,6 +360,7 @@ require ( go.uber.org/zap v1.26.0 // indirect golang.org/x/arch v0.4.0 // indirect golang.org/x/crypto v0.31.0 // indirect + golang.org/x/exp v0.0.0-20241210194714-1829a127f884 // indirect golang.org/x/oauth2 v0.24.0 // indirect golang.org/x/term v0.27.0 // indirect google.golang.org/api v0.214.0 // indirect diff --git a/go.sum b/go.sum index 6c0789d4c..24adbf0a5 100644 --- a/go.sum +++ b/go.sum @@ -259,8 +259,6 @@ github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSV github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs= github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= -github.com/cpu/goacmedns v0.1.1 h1:DM3H2NiN2oam7QljgGY5ygy4yDXhK5Z4JUnqaugs2C4= -github.com/cpu/goacmedns v0.1.1/go.mod h1:MuaouqEhPAHxsbqjgnck5zeghuwBP1dLnPoobeGqugQ= github.com/cpuguy83/dockercfg v0.3.1 h1:/FpZ+JaygUR/lZP2NlFI2DVfrOEMAIKP5wWEJdoYe9E= github.com/cpuguy83/dockercfg v0.3.1/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc= github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= @@ -357,8 +355,8 @@ github.com/gin-gonic/gin v1.6.3/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwv github.com/gin-gonic/gin v1.7.4/go.mod h1:jD2toBW3GZUr5UMcdrwQA10I7RuaFOl/SGeDjXkfUtY= github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg= github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU= -github.com/go-acme/lego/v4 v4.21.0 h1:arEW+8o5p7VI8Bk1kr/PDlgD1DrxtTH1gJ4b7mehL8o= -github.com/go-acme/lego/v4 v4.21.0/go.mod h1:HrSWzm3Ckj45Ie3i+p1zKVobbQoMOaGu9m4up0dUeDI= +github.com/go-acme/lego/v4 v4.22.2 h1:ck+HllWrV/rZGeYohsKQ5iKNnU/WAZxwOdiu6cxky+0= +github.com/go-acme/lego/v4 v4.22.2/go.mod h1:E2FndyI3Ekv0usNJt46mFb9LVpV/XBYT+4E3tz02Tzo= github.com/go-chi/chi/v5 v5.0.0/go.mod h1:BBug9lr0cqtdAhsu6R4AAdvufI0/XBzAQSsUqJpoZOs= github.com/go-cmd/cmd v1.0.5/go.mod h1:y8q8qlK5wQibcw63djSl/ntiHUHXHGdCkPk0j4QeW4s= github.com/go-errors/errors v1.0.1 h1:LUHzmkK3GUKUrL/1gfBUxAHzcev3apQlezX/+O7ma6w= @@ -905,6 +903,8 @@ github.com/nrdcg/dnspod-go v0.4.0 h1:c/jn1mLZNKF3/osJ6mz3QPxTudvPArXTjpkmYj0uK6U github.com/nrdcg/dnspod-go v0.4.0/go.mod h1:vZSoFSFeQVm2gWLMkyX61LZ8HI3BaqtHZWgPTGKr6KQ= github.com/nrdcg/freemyip v0.3.0 h1:0D2rXgvLwe2RRaVIjyUcQ4S26+cIS2iFwnhzDsEuuwc= github.com/nrdcg/freemyip v0.3.0/go.mod h1:c1PscDvA0ukBF0dwelU/IwOakNKnVxetpAQ863RMJoM= +github.com/nrdcg/goacmedns v0.2.0 h1:ADMbThobzEMnr6kg2ohs4KGa3LFqmgiBA22/6jUWJR0= +github.com/nrdcg/goacmedns v0.2.0/go.mod h1:T5o6+xvSLrQpugmwHvrSNkzWht0UGAwj2ACBMhh73Cg= github.com/nrdcg/goinwx v0.10.0 h1:6W630bjDxQD6OuXKqrFRYVpTt0G/9GXXm3CeOrN0zJM= github.com/nrdcg/goinwx v0.10.0/go.mod h1:mnMSTi7CXBu2io4DzdOBoGFA1XclD0sEPWJaDhNgkA4= github.com/nrdcg/mailinabox v0.2.0 h1:IKq8mfKiVwNW2hQii/ng1dJ4yYMMv3HAP3fMFIq2CFk= @@ -1187,8 +1187,8 @@ github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9f github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= github.com/traefik/grpc-web v0.16.0 h1:eeUWZaFg6ZU0I9dWOYE2D5qkNzRBmXzzuRlxdltascY= github.com/traefik/grpc-web v0.16.0/go.mod h1:2ttniSv7pTgBWIU2HZLokxRfFX3SA60c/DTmQQgVml4= -github.com/traefik/paerser v0.2.1 h1:LFgeak1NmjEHF53c9ENdXdL1UMkF/lD5t+7Evsz4hH4= -github.com/traefik/paerser v0.2.1/go.mod h1:7BBDd4FANoVgaTZG+yh26jI6CA2nds7D/4VTEdIsh24= +github.com/traefik/paerser v0.2.2 h1:cpzW/ZrQrBh3mdwD/jnp6aXASiUFKOVr6ldP+keJTcQ= +github.com/traefik/paerser v0.2.2/go.mod h1:7BBDd4FANoVgaTZG+yh26jI6CA2nds7D/4VTEdIsh24= github.com/traefik/yaegi v0.16.1 h1:f1De3DVJqIDKmnasUF6MwmWv1dSEEat0wcpXhD2On3E= github.com/traefik/yaegi v0.16.1/go.mod h1:4eVhbPb3LnD2VigQjhYbEJ69vDRFdT2HQNrXx8eEwUY= github.com/transip/gotransip/v6 v6.26.0 h1:Aejfvh8rSp8Mj2GX/RpdBjMCv+Iy/DmgfNgczPDP550= diff --git a/integration/fixtures/acme/acme_domains_1712362990.toml b/integration/fixtures/acme/acme_domains_1712362990.toml new file mode 100644 index 000000000..298ba1478 --- /dev/null +++ b/integration/fixtures/acme/acme_domains_1712362990.toml @@ -0,0 +1,57 @@ +[global] + checkNewVersion = false + sendAnonymousUsage = false + +[log] + level = "DEBUG" + noColor = true + +[entryPoints] + [entryPoints.web] + address = ":5002" + [entryPoints.websecure] + address = ":5001" + + + +[certificatesResolvers.default.acme] + email = "test@traefik.io" + storage = "/tmp/acme.json" + keyType = "" + caServer = "https://172.31.42.2:14000/dir" + + + + + [certificatesResolvers.default.acme.tlsChallenge] + + + + +[api] + insecure = true + +[providers.file] + filename = "fixtures/acme/acme_domains_1712362990.toml" + +## dynamic configuration ## + +[http.services] + [http.services.test.loadBalancer] + [[http.services.test.loadBalancer.servers]] + url = "http://127.0.0.1:9010" + +[http.routers] + [http.routers.test] + entryPoints = ["websecure"] + rule = "PathPrefix(`/`)" + service = "test" + [http.routers.test.tls] + certResolver = "default" + + [[http.routers.test.tls.domains]] + main = "acme.wtf" + sans = [ + "traefik.acme.wtf", + ] + diff --git a/integration/fixtures/tracing/simple-opentelemetry.toml b/integration/fixtures/tracing/simple-opentelemetry.toml index 77cb436bb..a8643ebd7 100644 --- a/integration/fixtures/tracing/simple-opentelemetry.toml +++ b/integration/fixtures/tracing/simple-opentelemetry.toml @@ -50,7 +50,7 @@ Rule = "Path(`/basic`)" [http.routers.router1] Service = "service1" - Middlewares = ["retry", "ratelimit-1"] + Middlewares = ["ratelimit-1"] Rule = "Path(`/ratelimit`)" [http.routers.router2] Service = "service2" @@ -58,8 +58,12 @@ Rule = "Path(`/retry`)" [http.routers.router3] Service = "service3" - Middlewares = ["retry", "basic-auth"] + Middlewares = ["basic-auth"] Rule = "Path(`/auth`)" + [http.routers.router4] + Service = "service4" + Middlewares = ["retry", "basic-auth"] + Rule = "Path(`/retry-auth`)" [http.routers.customPing] entryPoints = ["web"] rule = "PathPrefix(`/ping`)" @@ -98,3 +102,9 @@ passHostHeader = true [[http.services.service3.loadBalancer.servers]] url = "http://{{.WhoamiIP}}:{{.WhoamiPort}}" + + [http.services.service4] + [http.services.service4.loadBalancer] + passHostHeader = true + [[http.services.service4.loadBalancer.servers]] + url = "http://{{.WhoamiIP}}:{{.WhoamiPort}}" diff --git a/integration/tracing_test.go b/integration/tracing_test.go index 7d1d0ff0b..d44ed0d0b 100644 --- a/integration/tracing_test.go +++ b/integration/tracing_test.go @@ -77,7 +77,7 @@ func (s *TracingSuite) TearDownTest() { s.composeStop("tempo") } -func (s *TracingSuite) TestOpentelemetryBasic_HTTP() { +func (s *TracingSuite) TestOpenTelemetryBasic_HTTP() { file := s.adaptFile("fixtures/tracing/simple-opentelemetry.toml", TracingTemplate{ WhoamiIP: s.whoamiIP, WhoamiPort: s.whoamiPort, @@ -144,7 +144,7 @@ func (s *TracingSuite) TestOpentelemetryBasic_HTTP() { s.checkTraceContent(contains) } -func (s *TracingSuite) TestOpentelemetryBasic_gRPC() { +func (s *TracingSuite) TestOpenTelemetryBasic_gRPC() { file := s.adaptFile("fixtures/tracing/simple-opentelemetry.toml", TracingTemplate{ WhoamiIP: s.whoamiIP, WhoamiPort: s.whoamiPort, @@ -201,7 +201,7 @@ func (s *TracingSuite) TestOpentelemetryBasic_gRPC() { s.checkTraceContent(contains) } -func (s *TracingSuite) TestOpentelemetryRateLimit() { +func (s *TracingSuite) TestOpenTelemetryRateLimit() { file := s.adaptFile("fixtures/tracing/simple-opentelemetry.toml", TracingTemplate{ WhoamiIP: s.whoamiIP, WhoamiPort: s.whoamiPort, @@ -248,48 +248,26 @@ func (s *TracingSuite) TestOpentelemetryRateLimit() { "batches.0.scopeSpans.0.spans.0.kind": "SPAN_KIND_INTERNAL", "batches.0.scopeSpans.0.spans.0.attributes.#(key=\"traefik.middleware.name\").value.stringValue": "ratelimit-1@file", - "batches.0.scopeSpans.0.spans.1.name": "Retry", + "batches.0.scopeSpans.0.spans.1.name": "Router", "batches.0.scopeSpans.0.spans.1.kind": "SPAN_KIND_INTERNAL", - "batches.0.scopeSpans.0.spans.1.attributes.#(key=\"traefik.middleware.name\").value.stringValue": "retry@file", + "batches.0.scopeSpans.0.spans.1.attributes.#(key=\"traefik.service.name\").value.stringValue": "service1@file", + "batches.0.scopeSpans.0.spans.1.attributes.#(key=\"traefik.router.name\").value.stringValue": "router1@file", + "batches.0.scopeSpans.0.spans.1.attributes.#(key=\"http.route\").value.stringValue": "Path(`/ratelimit`)", - "batches.0.scopeSpans.0.spans.2.name": "RateLimiter", + "batches.0.scopeSpans.0.spans.2.name": "Metrics", "batches.0.scopeSpans.0.spans.2.kind": "SPAN_KIND_INTERNAL", - "batches.0.scopeSpans.0.spans.2.attributes.#(key=\"traefik.middleware.name\").value.stringValue": "ratelimit-1@file", + "batches.0.scopeSpans.0.spans.2.attributes.#(key=\"traefik.middleware.name\").value.stringValue": "metrics-entrypoint", - "batches.0.scopeSpans.0.spans.3.name": "Retry", - "batches.0.scopeSpans.0.spans.3.kind": "SPAN_KIND_INTERNAL", - "batches.0.scopeSpans.0.spans.3.attributes.#(key=\"traefik.middleware.name\").value.stringValue": "retry@file", - "batches.0.scopeSpans.0.spans.3.attributes.#(key=\"http.request.resend_count\").value.intValue": "1", - - "batches.0.scopeSpans.0.spans.4.name": "RateLimiter", - "batches.0.scopeSpans.0.spans.4.kind": "SPAN_KIND_INTERNAL", - "batches.0.scopeSpans.0.spans.4.attributes.#(key=\"traefik.middleware.name\").value.stringValue": "ratelimit-1@file", - - "batches.0.scopeSpans.0.spans.5.name": "Retry", - "batches.0.scopeSpans.0.spans.5.kind": "SPAN_KIND_INTERNAL", - "batches.0.scopeSpans.0.spans.5.attributes.#(key=\"traefik.middleware.name\").value.stringValue": "retry@file", - "batches.0.scopeSpans.0.spans.5.attributes.#(key=\"http.request.resend_count\").value.intValue": "2", - - "batches.0.scopeSpans.0.spans.6.name": "Router", - "batches.0.scopeSpans.0.spans.6.kind": "SPAN_KIND_INTERNAL", - "batches.0.scopeSpans.0.spans.6.attributes.#(key=\"traefik.service.name\").value.stringValue": "service1@file", - "batches.0.scopeSpans.0.spans.6.attributes.#(key=\"traefik.router.name\").value.stringValue": "router1@file", - "batches.0.scopeSpans.0.spans.6.attributes.#(key=\"http.route\").value.stringValue": "Path(`/ratelimit`)", - - "batches.0.scopeSpans.0.spans.7.name": "Metrics", - "batches.0.scopeSpans.0.spans.7.kind": "SPAN_KIND_INTERNAL", - "batches.0.scopeSpans.0.spans.7.attributes.#(key=\"traefik.middleware.name\").value.stringValue": "metrics-entrypoint", - - "batches.0.scopeSpans.0.spans.8.name": "EntryPoint", - "batches.0.scopeSpans.0.spans.8.kind": "SPAN_KIND_SERVER", - "batches.0.scopeSpans.0.spans.8.attributes.#(key=\"entry_point\").value.stringValue": "web", - "batches.0.scopeSpans.0.spans.8.attributes.#(key=\"http.request.method\").value.stringValue": "GET", - "batches.0.scopeSpans.0.spans.8.attributes.#(key=\"url.path\").value.stringValue": "/ratelimit", - "batches.0.scopeSpans.0.spans.8.attributes.#(key=\"url.query\").value.stringValue": "", - "batches.0.scopeSpans.0.spans.8.attributes.#(key=\"user_agent.original\").value.stringValue": "Go-http-client/1.1", - "batches.0.scopeSpans.0.spans.8.attributes.#(key=\"server.address\").value.stringValue": "127.0.0.1:8000", - "batches.0.scopeSpans.0.spans.8.attributes.#(key=\"network.peer.address\").value.stringValue": "127.0.0.1", - "batches.0.scopeSpans.0.spans.8.attributes.#(key=\"http.response.status_code\").value.intValue": "429", + "batches.0.scopeSpans.0.spans.3.name": "EntryPoint", + "batches.0.scopeSpans.0.spans.3.kind": "SPAN_KIND_SERVER", + "batches.0.scopeSpans.0.spans.3.attributes.#(key=\"entry_point\").value.stringValue": "web", + "batches.0.scopeSpans.0.spans.3.attributes.#(key=\"http.request.method\").value.stringValue": "GET", + "batches.0.scopeSpans.0.spans.3.attributes.#(key=\"url.path\").value.stringValue": "/ratelimit", + "batches.0.scopeSpans.0.spans.3.attributes.#(key=\"url.query\").value.stringValue": "", + "batches.0.scopeSpans.0.spans.3.attributes.#(key=\"user_agent.original\").value.stringValue": "Go-http-client/1.1", + "batches.0.scopeSpans.0.spans.3.attributes.#(key=\"server.address\").value.stringValue": "127.0.0.1:8000", + "batches.0.scopeSpans.0.spans.3.attributes.#(key=\"network.peer.address\").value.stringValue": "127.0.0.1", + "batches.0.scopeSpans.0.spans.3.attributes.#(key=\"http.response.status_code\").value.intValue": "429", }, { "batches.0.scopeSpans.0.scope.name": "github.com/traefik/traefik", @@ -318,37 +296,33 @@ func (s *TracingSuite) TestOpentelemetryRateLimit() { "batches.0.scopeSpans.0.spans.3.kind": "SPAN_KIND_INTERNAL", "batches.0.scopeSpans.0.spans.3.attributes.#(key=\"traefik.middleware.name\").value.stringValue": "ratelimit-1@file", - "batches.0.scopeSpans.0.spans.4.name": "Retry", + "batches.0.scopeSpans.0.spans.4.name": "Router", "batches.0.scopeSpans.0.spans.4.kind": "SPAN_KIND_INTERNAL", - "batches.0.scopeSpans.0.spans.4.attributes.#(key=\"traefik.middleware.name\").value.stringValue": "retry@file", + "batches.0.scopeSpans.0.spans.4.attributes.#(key=\"traefik.service.name\").value.stringValue": "service1@file", + "batches.0.scopeSpans.0.spans.4.attributes.#(key=\"traefik.router.name\").value.stringValue": "router1@file", + "batches.0.scopeSpans.0.spans.4.attributes.#(key=\"http.route\").value.stringValue": "Path(`/ratelimit`)", - "batches.0.scopeSpans.0.spans.5.name": "Router", + "batches.0.scopeSpans.0.spans.5.name": "Metrics", "batches.0.scopeSpans.0.spans.5.kind": "SPAN_KIND_INTERNAL", - "batches.0.scopeSpans.0.spans.5.attributes.#(key=\"traefik.service.name\").value.stringValue": "service1@file", - "batches.0.scopeSpans.0.spans.5.attributes.#(key=\"traefik.router.name\").value.stringValue": "router1@file", - "batches.0.scopeSpans.0.spans.5.attributes.#(key=\"http.route\").value.stringValue": "Path(`/ratelimit`)", + "batches.0.scopeSpans.0.spans.5.attributes.#(key=\"traefik.middleware.name\").value.stringValue": "metrics-entrypoint", - "batches.0.scopeSpans.0.spans.6.name": "Metrics", - "batches.0.scopeSpans.0.spans.6.kind": "SPAN_KIND_INTERNAL", - "batches.0.scopeSpans.0.spans.6.attributes.#(key=\"traefik.middleware.name\").value.stringValue": "metrics-entrypoint", - - "batches.0.scopeSpans.0.spans.7.name": "EntryPoint", - "batches.0.scopeSpans.0.spans.7.kind": "SPAN_KIND_SERVER", - "batches.0.scopeSpans.0.spans.7.attributes.#(key=\"entry_point\").value.stringValue": "web", - "batches.0.scopeSpans.0.spans.7.attributes.#(key=\"http.request.method\").value.stringValue": "GET", - "batches.0.scopeSpans.0.spans.7.attributes.#(key=\"url.path\").value.stringValue": "/ratelimit", - "batches.0.scopeSpans.0.spans.7.attributes.#(key=\"url.query\").value.stringValue": "", - "batches.0.scopeSpans.0.spans.7.attributes.#(key=\"user_agent.original\").value.stringValue": "Go-http-client/1.1", - "batches.0.scopeSpans.0.spans.7.attributes.#(key=\"server.address\").value.stringValue": "127.0.0.1:8000", - "batches.0.scopeSpans.0.spans.7.attributes.#(key=\"network.peer.address\").value.stringValue": "127.0.0.1", - "batches.0.scopeSpans.0.spans.7.attributes.#(key=\"http.response.status_code\").value.intValue": "200", + "batches.0.scopeSpans.0.spans.6.name": "EntryPoint", + "batches.0.scopeSpans.0.spans.6.kind": "SPAN_KIND_SERVER", + "batches.0.scopeSpans.0.spans.6.attributes.#(key=\"entry_point\").value.stringValue": "web", + "batches.0.scopeSpans.0.spans.6.attributes.#(key=\"http.request.method\").value.stringValue": "GET", + "batches.0.scopeSpans.0.spans.6.attributes.#(key=\"url.path\").value.stringValue": "/ratelimit", + "batches.0.scopeSpans.0.spans.6.attributes.#(key=\"url.query\").value.stringValue": "", + "batches.0.scopeSpans.0.spans.6.attributes.#(key=\"user_agent.original\").value.stringValue": "Go-http-client/1.1", + "batches.0.scopeSpans.0.spans.6.attributes.#(key=\"server.address\").value.stringValue": "127.0.0.1:8000", + "batches.0.scopeSpans.0.spans.6.attributes.#(key=\"network.peer.address\").value.stringValue": "127.0.0.1", + "batches.0.scopeSpans.0.spans.6.attributes.#(key=\"http.response.status_code\").value.intValue": "200", }, } s.checkTraceContent(contains) } -func (s *TracingSuite) TestOpentelemetryRetry() { +func (s *TracingSuite) TestOpenTelemetryRetry() { file := s.adaptFile("fixtures/tracing/simple-opentelemetry.toml", TracingTemplate{ WhoamiIP: s.whoamiIP, WhoamiPort: 81, @@ -471,7 +445,7 @@ func (s *TracingSuite) TestOpentelemetryRetry() { s.checkTraceContent(contains) } -func (s *TracingSuite) TestOpentelemetryAuth() { +func (s *TracingSuite) TestOpenTelemetryAuth() { file := s.adaptFile("fixtures/tracing/simple-opentelemetry.toml", TracingTemplate{ WhoamiIP: s.whoamiIP, WhoamiPort: s.whoamiPort, @@ -498,59 +472,90 @@ func (s *TracingSuite) TestOpentelemetryAuth() { "batches.0.scopeSpans.0.spans.0.status.message": "Authentication failed", "batches.0.scopeSpans.0.spans.0.status.code": "STATUS_CODE_ERROR", - "batches.0.scopeSpans.0.spans.1.name": "Retry", + "batches.0.scopeSpans.0.spans.1.name": "Router", "batches.0.scopeSpans.0.spans.1.kind": "SPAN_KIND_INTERNAL", - "batches.0.scopeSpans.0.spans.1.attributes.#(key=\"traefik.middleware.name\").value.stringValue": "retry@file", + "batches.0.scopeSpans.0.spans.1.attributes.#(key=\"traefik.service.name\").value.stringValue": "service3@file", + "batches.0.scopeSpans.0.spans.1.attributes.#(key=\"traefik.router.name\").value.stringValue": "router3@file", + "batches.0.scopeSpans.0.spans.1.attributes.#(key=\"http.route\").value.stringValue": "Path(`/auth`)", - "batches.0.scopeSpans.0.spans.2.name": "BasicAuth", + "batches.0.scopeSpans.0.spans.2.name": "Metrics", "batches.0.scopeSpans.0.spans.2.kind": "SPAN_KIND_INTERNAL", - "batches.0.scopeSpans.0.spans.2.attributes.#(key=\"traefik.middleware.name\").value.stringValue": "basic-auth@file", - "batches.0.scopeSpans.0.spans.2.status.message": "Authentication failed", - "batches.0.scopeSpans.0.spans.2.status.code": "STATUS_CODE_ERROR", + "batches.0.scopeSpans.0.spans.2.attributes.#(key=\"traefik.middleware.name\").value.stringValue": "metrics-entrypoint", - "batches.0.scopeSpans.0.spans.3.name": "Retry", - "batches.0.scopeSpans.0.spans.3.kind": "SPAN_KIND_INTERNAL", - "batches.0.scopeSpans.0.spans.3.attributes.#(key=\"traefik.middleware.name\").value.stringValue": "retry@file", - "batches.0.scopeSpans.0.spans.3.attributes.#(key=\"http.request.resend_count\").value.intValue": "1", - - "batches.0.scopeSpans.0.spans.4.name": "BasicAuth", - "batches.0.scopeSpans.0.spans.4.kind": "SPAN_KIND_INTERNAL", - "batches.0.scopeSpans.0.spans.4.attributes.#(key=\"traefik.middleware.name\").value.stringValue": "basic-auth@file", - "batches.0.scopeSpans.0.spans.4.status.message": "Authentication failed", - "batches.0.scopeSpans.0.spans.4.status.code": "STATUS_CODE_ERROR", - - "batches.0.scopeSpans.0.spans.5.name": "Retry", - "batches.0.scopeSpans.0.spans.5.kind": "SPAN_KIND_INTERNAL", - "batches.0.scopeSpans.0.spans.5.attributes.#(key=\"traefik.middleware.name\").value.stringValue": "retry@file", - "batches.0.scopeSpans.0.spans.5.attributes.#(key=\"http.request.resend_count\").value.intValue": "2", - - "batches.0.scopeSpans.0.spans.6.name": "Router", - "batches.0.scopeSpans.0.spans.6.kind": "SPAN_KIND_INTERNAL", - "batches.0.scopeSpans.0.spans.6.attributes.#(key=\"traefik.service.name\").value.stringValue": "service3@file", - "batches.0.scopeSpans.0.spans.6.attributes.#(key=\"traefik.router.name\").value.stringValue": "router3@file", - "batches.0.scopeSpans.0.spans.6.attributes.#(key=\"http.route\").value.stringValue": "Path(`/auth`)", - - "batches.0.scopeSpans.0.spans.7.name": "Metrics", - "batches.0.scopeSpans.0.spans.7.kind": "SPAN_KIND_INTERNAL", - "batches.0.scopeSpans.0.spans.7.attributes.#(key=\"traefik.middleware.name\").value.stringValue": "metrics-entrypoint", - - "batches.0.scopeSpans.0.spans.8.name": "EntryPoint", - "batches.0.scopeSpans.0.spans.8.kind": "SPAN_KIND_SERVER", - "batches.0.scopeSpans.0.spans.8.attributes.#(key=\"entry_point\").value.stringValue": "web", - "batches.0.scopeSpans.0.spans.8.attributes.#(key=\"http.request.method\").value.stringValue": "GET", - "batches.0.scopeSpans.0.spans.8.attributes.#(key=\"url.path\").value.stringValue": "/auth", - "batches.0.scopeSpans.0.spans.8.attributes.#(key=\"url.query\").value.stringValue": "", - "batches.0.scopeSpans.0.spans.8.attributes.#(key=\"user_agent.original\").value.stringValue": "Go-http-client/1.1", - "batches.0.scopeSpans.0.spans.8.attributes.#(key=\"server.address\").value.stringValue": "127.0.0.1:8000", - "batches.0.scopeSpans.0.spans.8.attributes.#(key=\"network.peer.address\").value.stringValue": "127.0.0.1", - "batches.0.scopeSpans.0.spans.8.attributes.#(key=\"http.response.status_code\").value.intValue": "401", + "batches.0.scopeSpans.0.spans.3.name": "EntryPoint", + "batches.0.scopeSpans.0.spans.3.kind": "SPAN_KIND_SERVER", + "batches.0.scopeSpans.0.spans.3.attributes.#(key=\"entry_point\").value.stringValue": "web", + "batches.0.scopeSpans.0.spans.3.attributes.#(key=\"http.request.method\").value.stringValue": "GET", + "batches.0.scopeSpans.0.spans.3.attributes.#(key=\"url.path\").value.stringValue": "/auth", + "batches.0.scopeSpans.0.spans.3.attributes.#(key=\"url.query\").value.stringValue": "", + "batches.0.scopeSpans.0.spans.3.attributes.#(key=\"user_agent.original\").value.stringValue": "Go-http-client/1.1", + "batches.0.scopeSpans.0.spans.3.attributes.#(key=\"server.address\").value.stringValue": "127.0.0.1:8000", + "batches.0.scopeSpans.0.spans.3.attributes.#(key=\"network.peer.address\").value.stringValue": "127.0.0.1", + "batches.0.scopeSpans.0.spans.3.attributes.#(key=\"http.response.status_code\").value.intValue": "401", }, } s.checkTraceContent(contains) } -func (s *TracingSuite) TestOpentelemetrySafeURL() { +func (s *TracingSuite) TestOpenTelemetryAuthWithRetry() { + file := s.adaptFile("fixtures/tracing/simple-opentelemetry.toml", TracingTemplate{ + WhoamiIP: s.whoamiIP, + WhoamiPort: s.whoamiPort, + IP: s.otelCollectorIP, + }) + defer os.Remove(file) + + s.traefikCmd(withConfigFile(file)) + + // wait for traefik + err := try.GetRequest("http://127.0.0.1:8080/api/rawdata", time.Second, try.BodyContains("basic-auth")) + require.NoError(s.T(), err) + + err = try.GetRequest("http://127.0.0.1:8000/retry-auth", 500*time.Millisecond, try.StatusCodeIs(http.StatusUnauthorized)) + require.NoError(s.T(), err) + + contains := []map[string]string{ + { + "batches.0.scopeSpans.0.scope.name": "github.com/traefik/traefik", + + "batches.0.scopeSpans.0.spans.0.name": "BasicAuth", + "batches.0.scopeSpans.0.spans.0.kind": "SPAN_KIND_INTERNAL", + "batches.0.scopeSpans.0.spans.0.attributes.#(key=\"traefik.middleware.name\").value.stringValue": "basic-auth@file", + "batches.0.scopeSpans.0.spans.0.status.message": "Authentication failed", + "batches.0.scopeSpans.0.spans.0.status.code": "STATUS_CODE_ERROR", + + "batches.0.scopeSpans.0.spans.1.name": "Retry", + "batches.0.scopeSpans.0.spans.1.kind": "SPAN_KIND_INTERNAL", + "batches.0.scopeSpans.0.spans.1.attributes.#(key=\"traefik.middleware.name\").value.stringValue": "retry@file", + + "batches.0.scopeSpans.0.spans.2.name": "Router", + "batches.0.scopeSpans.0.spans.2.kind": "SPAN_KIND_INTERNAL", + "batches.0.scopeSpans.0.spans.2.attributes.#(key=\"traefik.service.name\").value.stringValue": "service4@file", + "batches.0.scopeSpans.0.spans.2.attributes.#(key=\"traefik.router.name\").value.stringValue": "router4@file", + "batches.0.scopeSpans.0.spans.2.attributes.#(key=\"http.route\").value.stringValue": "Path(`/retry-auth`)", + + "batches.0.scopeSpans.0.spans.3.name": "Metrics", + "batches.0.scopeSpans.0.spans.3.kind": "SPAN_KIND_INTERNAL", + "batches.0.scopeSpans.0.spans.3.attributes.#(key=\"traefik.middleware.name\").value.stringValue": "metrics-entrypoint", + + "batches.0.scopeSpans.0.spans.4.name": "EntryPoint", + "batches.0.scopeSpans.0.spans.4.kind": "SPAN_KIND_SERVER", + "batches.0.scopeSpans.0.spans.4.attributes.#(key=\"entry_point\").value.stringValue": "web", + "batches.0.scopeSpans.0.spans.4.attributes.#(key=\"http.request.method\").value.stringValue": "GET", + "batches.0.scopeSpans.0.spans.4.attributes.#(key=\"url.path\").value.stringValue": "/retry-auth", + "batches.0.scopeSpans.0.spans.4.attributes.#(key=\"url.query\").value.stringValue": "", + "batches.0.scopeSpans.0.spans.4.attributes.#(key=\"user_agent.original\").value.stringValue": "Go-http-client/1.1", + "batches.0.scopeSpans.0.spans.4.attributes.#(key=\"server.address\").value.stringValue": "127.0.0.1:8000", + "batches.0.scopeSpans.0.spans.4.attributes.#(key=\"network.peer.address\").value.stringValue": "127.0.0.1", + "batches.0.scopeSpans.0.spans.4.attributes.#(key=\"http.response.status_code\").value.intValue": "401", + }, + } + + s.checkTraceContent(contains) +} + +func (s *TracingSuite) TestOpenTelemetrySafeURL() { file := s.adaptFile("fixtures/tracing/simple-opentelemetry.toml", TracingTemplate{ WhoamiIP: s.whoamiIP, WhoamiPort: s.whoamiPort, @@ -593,30 +598,26 @@ func (s *TracingSuite) TestOpentelemetrySafeURL() { "batches.0.scopeSpans.0.spans.3.kind": "SPAN_KIND_INTERNAL", "batches.0.scopeSpans.0.spans.3.attributes.#(key=\"traefik.middleware.name\").value.stringValue": "basic-auth@file", - "batches.0.scopeSpans.0.spans.4.name": "Retry", + "batches.0.scopeSpans.0.spans.4.name": "Router", "batches.0.scopeSpans.0.spans.4.kind": "SPAN_KIND_INTERNAL", - "batches.0.scopeSpans.0.spans.4.attributes.#(key=\"traefik.middleware.name\").value.stringValue": "retry@file", + "batches.0.scopeSpans.0.spans.4.attributes.#(key=\"traefik.service.name\").value.stringValue": "service3@file", + "batches.0.scopeSpans.0.spans.4.attributes.#(key=\"traefik.router.name\").value.stringValue": "router3@file", + "batches.0.scopeSpans.0.spans.4.attributes.#(key=\"http.route\").value.stringValue": "Path(`/auth`)", - "batches.0.scopeSpans.0.spans.5.name": "Router", + "batches.0.scopeSpans.0.spans.5.name": "Metrics", "batches.0.scopeSpans.0.spans.5.kind": "SPAN_KIND_INTERNAL", - "batches.0.scopeSpans.0.spans.5.attributes.#(key=\"traefik.service.name\").value.stringValue": "service3@file", - "batches.0.scopeSpans.0.spans.5.attributes.#(key=\"traefik.router.name\").value.stringValue": "router3@file", - "batches.0.scopeSpans.0.spans.5.attributes.#(key=\"http.route\").value.stringValue": "Path(`/auth`)", + "batches.0.scopeSpans.0.spans.5.attributes.#(key=\"traefik.middleware.name\").value.stringValue": "metrics-entrypoint", - "batches.0.scopeSpans.0.spans.6.name": "Metrics", - "batches.0.scopeSpans.0.spans.6.kind": "SPAN_KIND_INTERNAL", - "batches.0.scopeSpans.0.spans.6.attributes.#(key=\"traefik.middleware.name\").value.stringValue": "metrics-entrypoint", - - "batches.0.scopeSpans.0.spans.7.name": "EntryPoint", - "batches.0.scopeSpans.0.spans.7.kind": "SPAN_KIND_SERVER", - "batches.0.scopeSpans.0.spans.7.attributes.#(key=\"entry_point\").value.stringValue": "web", - "batches.0.scopeSpans.0.spans.7.attributes.#(key=\"http.request.method\").value.stringValue": "GET", - "batches.0.scopeSpans.0.spans.7.attributes.#(key=\"url.path\").value.stringValue": "/auth", - "batches.0.scopeSpans.0.spans.7.attributes.#(key=\"url.query\").value.stringValue": "api_key=REDACTED", - "batches.0.scopeSpans.0.spans.7.attributes.#(key=\"user_agent.original\").value.stringValue": "Go-http-client/1.1", - "batches.0.scopeSpans.0.spans.7.attributes.#(key=\"server.address\").value.stringValue": "127.0.0.1:8000", - "batches.0.scopeSpans.0.spans.7.attributes.#(key=\"network.peer.address\").value.stringValue": "127.0.0.1", - "batches.0.scopeSpans.0.spans.7.attributes.#(key=\"http.response.status_code\").value.intValue": "200", + "batches.0.scopeSpans.0.spans.6.name": "EntryPoint", + "batches.0.scopeSpans.0.spans.6.kind": "SPAN_KIND_SERVER", + "batches.0.scopeSpans.0.spans.6.attributes.#(key=\"entry_point\").value.stringValue": "web", + "batches.0.scopeSpans.0.spans.6.attributes.#(key=\"http.request.method\").value.stringValue": "GET", + "batches.0.scopeSpans.0.spans.6.attributes.#(key=\"url.path\").value.stringValue": "/auth", + "batches.0.scopeSpans.0.spans.6.attributes.#(key=\"url.query\").value.stringValue": "api_key=REDACTED", + "batches.0.scopeSpans.0.spans.6.attributes.#(key=\"user_agent.original\").value.stringValue": "Go-http-client/1.1", + "batches.0.scopeSpans.0.spans.6.attributes.#(key=\"server.address\").value.stringValue": "127.0.0.1:8000", + "batches.0.scopeSpans.0.spans.6.attributes.#(key=\"network.peer.address\").value.stringValue": "127.0.0.1", + "batches.0.scopeSpans.0.spans.6.attributes.#(key=\"http.response.status_code\").value.intValue": "200", }, } diff --git a/pkg/api/sort.go b/pkg/api/sort.go index bfa458f07..cf70679cd 100644 --- a/pkg/api/sort.go +++ b/pkg/api/sort.go @@ -1,10 +1,9 @@ package api import ( + "cmp" "net/url" "sort" - - "golang.org/x/exp/constraints" ) const ( @@ -357,7 +356,7 @@ func sortByName[T orderedWithName](direction string, results []T) { }) } -func sortByFunc[T orderedWithName, U constraints.Ordered](direction string, results []T, fn func(int) U) { +func sortByFunc[T orderedWithName, U cmp.Ordered](direction string, results []T, fn func(int) U) { // Ascending if direction == ascendantSorting { sort.Slice(results, func(i, j int) bool { diff --git a/pkg/middlewares/retry/retry.go b/pkg/middlewares/retry/retry.go index f462d2d70..068030777 100644 --- a/pkg/middlewares/retry/retry.go +++ b/pkg/middlewares/retry/retry.go @@ -36,6 +36,48 @@ type Listener interface { // each of them about a retry attempt. type Listeners []Listener +// Retried exists to implement the Listener interface. It calls Retried on each of its slice entries. +func (l Listeners) Retried(req *http.Request, attempt int) { + for _, listener := range l { + listener.Retried(req, attempt) + } +} + +type shouldRetryContextKey struct{} + +// ShouldRetry is a function allowing to enable/disable the retry middleware mechanism. +type ShouldRetry func(shouldRetry bool) + +// ContextShouldRetry returns the ShouldRetry function if it has been set by the Retry middleware in the chain. +func ContextShouldRetry(ctx context.Context) ShouldRetry { + f, _ := ctx.Value(shouldRetryContextKey{}).(ShouldRetry) + return f +} + +// WrapHandler wraps a given http.Handler to inject the httptrace.ClientTrace in the request context when it is needed +// by the retry middleware. +func WrapHandler(next http.Handler) http.Handler { + return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { + if shouldRetry := ContextShouldRetry(req.Context()); shouldRetry != nil { + shouldRetry(true) + + trace := &httptrace.ClientTrace{ + WroteHeaders: func() { + shouldRetry(false) + }, + WroteRequest: func(httptrace.WroteRequestInfo) { + shouldRetry(false) + }, + } + newCtx := httptrace.WithClientTrace(req.Context(), trace) + next.ServeHTTP(rw, req.WithContext(newCtx)) + return + } + + next.ServeHTTP(rw, req) + }) +} + // retry is a middleware that retries requests. type retry struct { attempts int @@ -101,19 +143,13 @@ func (r *retry) ServeHTTP(rw http.ResponseWriter, req *http.Request) { req = req.WithContext(tracingCtx) } - shouldRetry := attempts < r.attempts - retryResponseWriter := newResponseWriter(rw, shouldRetry) + remainAttempts := attempts < r.attempts + retryResponseWriter := newResponseWriter(rw) - // Disable retries when the backend already received request data - clientTrace := &httptrace.ClientTrace{ - WroteHeaders: func() { - retryResponseWriter.DisableRetries() - }, - WroteRequest: func(httptrace.WroteRequestInfo) { - retryResponseWriter.DisableRetries() - }, + var shouldRetry ShouldRetry = func(shouldRetry bool) { + retryResponseWriter.SetShouldRetry(remainAttempts && shouldRetry) } - newCtx := httptrace.WithClientTrace(req.Context(), clientTrace) + newCtx := context.WithValue(req.Context(), shouldRetryContextKey{}, shouldRetry) r.next.ServeHTTP(retryResponseWriter, req.Clone(newCtx)) @@ -164,18 +200,10 @@ func (r *retry) newBackOff() backoff.BackOff { return b } -// Retried exists to implement the Listener interface. It calls Retried on each of its slice entries. -func (l Listeners) Retried(req *http.Request, attempt int) { - for _, listener := range l { - listener.Retried(req, attempt) - } -} - -func newResponseWriter(rw http.ResponseWriter, shouldRetry bool) *responseWriter { +func newResponseWriter(rw http.ResponseWriter) *responseWriter { return &responseWriter{ responseWriter: rw, headers: make(http.Header), - shouldRetry: shouldRetry, } } @@ -190,8 +218,8 @@ func (r *responseWriter) ShouldRetry() bool { return r.shouldRetry } -func (r *responseWriter) DisableRetries() { - r.shouldRetry = false +func (r *responseWriter) SetShouldRetry(shouldRetry bool) { + r.shouldRetry = shouldRetry } func (r *responseWriter) Header() http.Header { @@ -205,20 +233,14 @@ func (r *responseWriter) Write(buf []byte) (int, error) { if r.ShouldRetry() { return len(buf), nil } + if !r.written { + r.WriteHeader(http.StatusOK) + } return r.responseWriter.Write(buf) } func (r *responseWriter) WriteHeader(code int) { - if r.ShouldRetry() && code == http.StatusServiceUnavailable { - // We get a 503 HTTP Status Code when there is no backend server in the pool - // to which the request could be sent. Also, note that r.ShouldRetry() - // will never return true in case there was a connection established to - // the backend server and so we can be sure that the 503 was produced - // inside Traefik already and we don't have to retry in this cases. - r.DisableRetries() - } - - if r.ShouldRetry() || r.written { + if r.shouldRetry || r.written { return } diff --git a/pkg/middlewares/retry/retry_test.go b/pkg/middlewares/retry/retry_test.go index 302415866..6dca141c6 100644 --- a/pkg/middlewares/retry/retry_test.go +++ b/pkg/middlewares/retry/retry_test.go @@ -105,12 +105,21 @@ func TestRetry(t *testing.T) { t.Parallel() retryAttempts := 0 - next := http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { + next := http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { + // This signals that a connection will be established with the backend + // to enable the Retry middleware mechanism. + shouldRetry := ContextShouldRetry(req.Context()) + if shouldRetry != nil { + shouldRetry(true) + } + retryAttempts++ if retryAttempts > test.amountFaultyEndpoints { - // calls WroteHeaders on httptrace. - _ = r.Write(io.Discard) + // This signals that request headers have been sent to the backend. + if shouldRetry != nil { + shouldRetry(false) + } rw.WriteHeader(http.StatusOK) return @@ -152,27 +161,16 @@ func TestRetryEmptyServerList(t *testing.T) { assert.Equal(t, 0, retryListener.timesCalled) } -func TestRetryListeners(t *testing.T) { - req := httptest.NewRequest(http.MethodGet, "/", nil) - retryListeners := Listeners{&countingRetryListener{}, &countingRetryListener{}} - - retryListeners.Retried(req, 1) - retryListeners.Retried(req, 1) - - for _, retryListener := range retryListeners { - listener := retryListener.(*countingRetryListener) - if listener.timesCalled != 2 { - t.Errorf("retry listener was called %d time(s), want %d time(s)", listener.timesCalled, 2) - } - } -} - func TestMultipleRetriesShouldNotLooseHeaders(t *testing.T) { attempt := 0 - expectedHeaderName := "X-Foo-Test-2" expectedHeaderValue := "bar" next := http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { + shouldRetry := ContextShouldRetry(req.Context()) + if shouldRetry != nil { + shouldRetry(true) + } + headerName := fmt.Sprintf("X-Foo-Test-%d", attempt) rw.Header().Add(headerName, expectedHeaderValue) if attempt < 2 { @@ -181,43 +179,54 @@ func TestMultipleRetriesShouldNotLooseHeaders(t *testing.T) { } // Request has been successfully written to backend - trace := httptrace.ContextClientTrace(req.Context()) - trace.WroteHeaders() + shouldRetry(false) - // And we decide to answer to client + // And we decide to answer to client. rw.WriteHeader(http.StatusNoContent) }) retry, err := New(context.Background(), next, dynamic.Retry{Attempts: 3}, &countingRetryListener{}, "traefikTest") require.NoError(t, err) - responseRecorder := httptest.NewRecorder() - retry.ServeHTTP(responseRecorder, testhelpers.MustNewRequest(http.MethodGet, "http://test", http.NoBody)) + res := httptest.NewRecorder() + retry.ServeHTTP(res, testhelpers.MustNewRequest(http.MethodGet, "http://test", http.NoBody)) - headerValue := responseRecorder.Header().Get(expectedHeaderName) - - // Validate if we have the correct header - if headerValue != expectedHeaderValue { - t.Errorf("Expected to have %s for header %s, got %s", expectedHeaderValue, expectedHeaderName, headerValue) - } + // The third header attempt is kept. + headerValue := res.Header().Get("X-Foo-Test-2") + assert.Equal(t, expectedHeaderValue, headerValue) // Validate that we don't have headers from previous attempts for i := range attempt { headerName := fmt.Sprintf("X-Foo-Test-%d", i) - headerValue = responseRecorder.Header().Get("headerName") + headerValue = res.Header().Get(headerName) if headerValue != "" { t.Errorf("Expected no value for header %s, got %s", headerName, headerValue) } } } -// countingRetryListener is a Listener implementation to count the times the Retried fn is called. -type countingRetryListener struct { - timesCalled int -} +func TestRetryShouldNotLooseHeadersOnWrite(t *testing.T) { + next := http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { + rw.Header().Add("X-Foo-Test", "bar") -func (l *countingRetryListener) Retried(req *http.Request, attempt int) { - l.timesCalled++ + // Request has been successfully written to backend. + shouldRetry := ContextShouldRetry(req.Context()) + if shouldRetry != nil { + shouldRetry(false) + } + // And we decide to answer to client without calling WriteHeader. + _, err := rw.Write([]byte("bar")) + require.NoError(t, err) + }) + + retry, err := New(context.Background(), next, dynamic.Retry{Attempts: 3}, &countingRetryListener{}, "traefikTest") + require.NoError(t, err) + + res := httptest.NewRecorder() + retry.ServeHTTP(res, testhelpers.MustNewRequest(http.MethodGet, "http://test", http.NoBody)) + + headerValue := res.Header().Get("X-Foo-Test") + assert.Equal(t, "bar", headerValue) } func TestRetryWithFlush(t *testing.T) { @@ -275,12 +284,24 @@ func TestRetryWebsocket(t *testing.T) { t.Parallel() retryAttempts := 0 - next := http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { + next := http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { + // This signals that a connection will be established with the backend + // to enable the Retry middleware mechanism. + shouldRetry := ContextShouldRetry(req.Context()) + if shouldRetry != nil { + shouldRetry(true) + } + retryAttempts++ if retryAttempts > test.amountFaultyEndpoints { + // This signals that request headers have been sent to the backend. + if shouldRetry != nil { + shouldRetry(false) + } + upgrader := websocket.Upgrader{} - _, err := upgrader.Upgrade(rw, r, nil) + _, err := upgrader.Upgrade(rw, req, nil) if err != nil { http.Error(rw, err.Error(), http.StatusInternalServerError) } @@ -387,3 +408,12 @@ func Test1xxResponses(t *testing.T) { assert.Equal(t, 0, retryListener.timesCalled) } + +// countingRetryListener is a Listener implementation to count the times the Retried fn is called. +type countingRetryListener struct { + timesCalled int +} + +func (l *countingRetryListener) Retried(req *http.Request, attempt int) { + l.timesCalled++ +} diff --git a/pkg/server/service/service.go b/pkg/server/service/service.go index 245cb08f6..a01d59416 100644 --- a/pkg/server/service/service.go +++ b/pkg/server/service/service.go @@ -22,6 +22,7 @@ import ( "github.com/traefik/traefik/v3/pkg/middlewares/capture" metricsMiddle "github.com/traefik/traefik/v3/pkg/middlewares/metrics" "github.com/traefik/traefik/v3/pkg/middlewares/observability" + "github.com/traefik/traefik/v3/pkg/middlewares/retry" "github.com/traefik/traefik/v3/pkg/proxy/httputil" "github.com/traefik/traefik/v3/pkg/safe" "github.com/traefik/traefik/v3/pkg/server/cookie" @@ -349,6 +350,10 @@ func (m *Manager) getLoadBalancerServiceHandler(ctx context.Context, serviceName if err != nil { return nil, fmt.Errorf("error building proxy for server URL %s: %w", server.URL, err) } + // The retry wrapping must be done just before the proxy handler, + // to make sure that the retry will not be triggered/disabled by + // middlewares in the chain. + proxy = retry.WrapHandler(proxy) // Prevents from enabling observability for internal resources.