Skip to content

api: add ClientTrafficPolicy option to emit Retry-After header on rate-limit 429 responses - #9770

Open
devilleweppenaar wants to merge 16 commits into
envoyproxy:mainfrom
devilleweppenaar:retry-after-header-ctp
Open

devilleweppenaar wants to merge 16 commits into
envoyproxy:mainfrom
devilleweppenaar:retry-after-header-ctp

Conversation

@devilleweppenaar

@devilleweppenaar devilleweppenaar commented Aug 17, 2026

Copy link
Copy Markdown

What this PR does / why we need it:

Envoy's global and local rate limit HTTP filters now support enable_retry_after_header (envoyproxy/envoy#46289), emitting a Retry-After header on rate-limited 429 responses. This adds ClientTrafficPolicy.spec.headers.disableRetryAfterHeader (bool, default false) to control it — the header is emitted by default, and can be opted out per listener, mirroring the existing disableRateLimitHeaders field end to end (API type -> IR -> both rate limit filter translators).

Per discussion on this PR, the field was renamed from enableRetryAfterHeader (opt-in, default false) to disableRetryAfterHeader (opt-out, default false/emitted) for consistency with disableRateLimitHeaders, while keeping X-RateLimit-* off, Retry-After on independently expressible.

Per-rule granularity (mirroring xRateLimitHeaders, #8279) is tracked separately in #9078 and needs further upstream Envoy work; out of scope here.

Which issue(s) this PR fixes:

Fixes #9769


PR Checklist

  • Authorship & ownership: Coding agents / AI assistants are welcome, but I have reviewed every change, understand how and why it works, can explain and maintain it, and take full responsibility for this PR. I have not submitted generated output I do not understand.
  • DCO: All commits are signed off (git commit -s).
  • API agreed first: Not yet explicitly agreed by a maintainer. The proposed shape was posted in ClientTrafficPolicy: option to emit Retry-After header on rate-limit 429 responses #9769 prior to implementation and mirrors already-accepted precedent (disableRateLimitHeaders, Feature Request: Granular Control for X-RateLimit Headers in BackendTrafficPolicy #8279); open to adjusting on review.
  • Required checks pass: make generate gen-check, make lint, and go test ./... pass locally.
  • Tests added/updated: gatewayapi golden-file case plus xds translator golden-file cases for both the global and local rate limit filters.
  • Docs: API reference doc is auto-generated (make generate); no additional user-facing docs page exists for the sibling disableRateLimitHeaders field either.
  • Release notes: added under release-notes/current/new_features/.
  • Generated files committed: CRDs, deepcopy, helm-template golden output all regenerated and committed.
  • Scope & compatibility: additive; Retry-After is emitted by default, disableRetryAfterHeader opts out.
  • Codex review: not yet requested.
  • Copilot review: not yet requested.

Bumps github.com/envoyproxy/go-control-plane/envoy to a commit built
after envoyproxy/envoy#46289 merged, which added
enable_retry_after_header to the global and local rate limit HTTP
filter protos. Needed before those fields can be referenced from Go.

extensions.gen.go picks up a few newly available extension packages
as an incidental side effect of the bump; unrelated to this change.

Signed-off-by: De Ville Weppenaar <11049609+devilleweppenaar@users.noreply.github.com>
Adds ClientTrafficPolicy.spec.headers.enableRetryAfterHeader, mirroring
the existing disableRateLimitHeaders field. Defaults to false.

Fixes envoyproxy#9769

Signed-off-by: De Ville Weppenaar <11049609+devilleweppenaar@users.noreply.github.com>
Threads ClientTrafficPolicy.spec.headers.enableRetryAfterHeader through
the IR and sets enable_retry_after_header on the generated global and
local rate limit HTTP filter configs, mirroring how
disableRateLimitHeaders is wired to enable_x_ratelimit_headers.

Signed-off-by: De Ville Weppenaar <11049609+devilleweppenaar@users.noreply.github.com>
Adds a ClientTrafficPolicy golden-file case exercising the new field
through the CRD-to-IR translation, and two xds-ir golden-file cases
covering the global and local rate limit filters.

Signed-off-by: De Ville Weppenaar <11049609+devilleweppenaar@users.noreply.github.com>
Signed-off-by: De Ville Weppenaar <11049609+devilleweppenaar@users.noreply.github.com>
Follow-up to the enableRetryAfterHeader CRD field: make helm-template
picks up the new field in the rendered gateway-crds-helm test fixtures.

Signed-off-by: De Ville Weppenaar <11049609+devilleweppenaar@users.noreply.github.com>
Companion to the root module bump so 'make gen-check' stays clean.

Signed-off-by: De Ville Weppenaar <11049609+devilleweppenaar@users.noreply.github.com>
Companion to the root module bump so the test module's go.mod stays
tidy and 'make lint' can load it.

Signed-off-by: De Ville Weppenaar <11049609+devilleweppenaar@users.noreply.github.com>
@devilleweppenaar
devilleweppenaar requested a review from a team as a code owner August 17, 2026 20:07
@netlify

netlify Bot commented Aug 17, 2026

Copy link
Copy Markdown

Deploy Preview for cerulean-figolla-1f9435 ready!

Name Link
🔨 Latest commit 453d681
🔍 Latest deploy log https://app.netlify.com/projects/cerulean-figolla-1f9435/deploys/6ab3321023e08c00088b6e0e
😎 Deploy Preview https://deploy-preview-9770--cerulean-figolla-1f9435.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 29a11ce516

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread api/v1alpha1/clienttrafficpolicy_types.go Outdated
Match the disableRateLimitHeaders convention instead of restating the
default, which the boolean type and the generated docs table already
convey.

Signed-off-by: De Ville Weppenaar <11049609+devilleweppenaar@users.noreply.github.com>
Adds conformance coverage for enableRetryAfterHeader against a real
Envoy, for both the global and local rate limit filters, addressing
review feedback that the existing coverage stopped at IR/xDS golden
serialization and never exercised runtime behavior.

Mirrors RateLimitHeadersDisabled's structure (ratelimit.go) and the
eventual-consistency retry idiom used elsewhere in local_ratelimit.go.
Retry-After's value is asserted with a small tolerance rather than an
exact match, since it counts down from the window size.

Signed-off-by: De Ville Weppenaar <11049609+devilleweppenaar@users.noreply.github.com>
The API doc comment said "when rate limiting is enabled", copied from
disableRateLimitHeaders, but that's inaccurate here: X-RateLimit-*
headers are considered on every response the filter evaluates, while
Retry-After is scoped to rate-limited 429 responses only, per Envoy's
own field doc. Every other place we described this option (IR,
translator, e2e tests, release note) already said "rate-limited 429
responses" -- bring the public API doc in line with that instead of
the other way around. Also trims a redundant "if explicitly enabled"
from the translator comment.

Signed-off-by: De Ville Weppenaar <11049609+devilleweppenaar@users.noreply.github.com>
@devilleweppenaar

Copy link
Copy Markdown
Author

I did a review of each edited file, including generated ones.

I also tested the changes locally using a kind cluster with all of the latest changes to ensure that the intended behavior:

  • disableRateLimitHeaders: true: no x-ratelimit-* headers on either the 200s or the 429.
  • enableRetryAfterHeader: true: no retry-after on the 200 responses; present on the 429 (retry-after: 1794) once the 2-request/hour local rate limit was exhausted.

This was running the built code from the PR branch (docker.io/envoyproxy/gateway-dev:19668e189, the current branch HEAD) against a real Envoy proxy in the local kind cluster.

@codecov

codecov Bot commented Aug 29, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 81.32%. Comparing base (01f026a) to head (648d2d4).
⚠️ Report is 4 commits behind head on main.

Additional details and impacted files
@@           Coverage Diff           @@
##             main    #9770   +/-   ##
=======================================
  Coverage   81.31%   81.32%           
=======================================
  Files         264      264           
  Lines       40973    40980    +7     
=======================================
+ Hits        33319    33328    +9     
+ Misses       7653     7651    -2     
  Partials        1        1           

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Resolve go.mod/go.sum conflicts in examples/extension-server and test
modules by taking main's dependency versions and re-running
'go mod tidy', which resolves consistently with the go-control-plane
bump via each module's local replace directive.

Signed-off-by: De Ville Weppenaar <11049609+devilleweppenaar@users.noreply.github.com>
@arkodg

arkodg commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

can we build on top of #8742 instead of adding the setting in CTP

@arkodg
arkodg requested a review from zirain August 31, 2026 04:37
@devilleweppenaar

Copy link
Copy Markdown
Author

can we build on top of #8742 instead of adding the setting in CTP

@arkodg I believe we could.

The main reason I went with the CTP option first is that I was copying the pattern of how disableRateLimitHeaders is accessed under HeaderSettings.

@devilleweppenaar

Copy link
Copy Markdown
Author

@arkodg I took another look and recalled why what you suggested is not currently possible.

Per-rule granularity is tracked separately in #9078, and it's blocked on upstream: envoyproxy/envoy#46289 added enable_retry_after_header as a filter-wide bool on RateLimit/LocalRateLimit only, not a per-action field on config.route.v3.RateLimit. xRateLimitHeaders (#8279) works per-rule because XRatelimitOption exists specifically on that action type; there's no equivalent for Retry-After yet.

This PR is scoped to the filter-wide CTP option for that reason, mirroring disableRateLimitHeaders. Once Envoy adds the per-action field, BTP per-rule support can land additively on top of this, tracked in #9078.

@arkodg

arkodg commented Sep 7, 2026

Copy link
Copy Markdown
Contributor

can we evaluate the tradeoffs & correctness of setting this field by default instead of enableRetryAfterHeader and opting out using disableRateLimitHeaders

@devilleweppenaar

devilleweppenaar commented Sep 7, 2026

Copy link
Copy Markdown
Author

@arkodg I think there's a variant that gets us the consistency without losing expressiveness.

Rather than reusing disableRateLimitHeaders to control Retry-After too, we can rename enableRetryAfterHeader to disableRetryAfterHeader and keep the default false: This means Retry-After is included by default on any rate-limited listener, and opt-out is managed via the new bool. This mirrors disableRateLimitHeaders in name, shape, and default value (unset/false = header on, true = suppressed), so HeaderSettings ends up with two structurally identical fields instead of one disable*/enable* pair with opposite default value.

This addresses your point (no explicit opt-in needed, matches the X-RateLimit-* precedent) while keeping the two headers independently controllable, which matters because they're not equivalent standards-wise: Retry-After (RFC 9110 section 10.2.3) is a stable, general-purpose HTTP mechanism reused across 429/503/3xx with broad client support, while X-RateLimit-/RateLimit- (draft-ietf-httpapi-ratelimit-headers) is still a WG draft that discloses actual quota thresholds. The draft itself treats them as separate-but-coexisting signals with defined precedence when both appear, not one on/off switch.

Bundling them under a single flag also makes "X-RateLimit-* off, Retry-After on" (a real use case: avoid disclosing exact limits, still let clients back off gracefully) inexpressible.

I am happy to update the PR to reflect the default on Retry-After, and to update the CTP flag to disableRetryAfterHeader instead, if that makes more sense to you too.

@arkodg

arkodg commented Sep 14, 2026

Copy link
Copy Markdown
Contributor

hey @devilleweppenaar +1 to opt out approach using disableRetryAfterHeader

# Conflicts:
#	examples/extension-server/go.mod
#	examples/extension-server/go.sum
#	go.mod
#	go.sum
#	test/e2e/tests/ratelimit.go
#	test/go.mod
#	test/go.sum
Emit the Retry-After header by default on rate-limited 429 responses,
opting out via ClientTrafficPolicy.spec.headers.disableRetryAfterHeader
instead of opting in. This mirrors disableRateLimitHeaders and keeps
"X-RateLimit-* off, Retry-After on" expressible, per review feedback
on envoyproxy#9770.

Signed-off-by: De Ville Weppenaar <11049609+devilleweppenaar@users.noreply.github.com>
Signed-off-by: De Ville Weppenaar <11049609+devilleweppenaar@users.noreply.github.com>
@devilleweppenaar

Copy link
Copy Markdown
Author

Hey @arkodg, I've made the proposed changes to this PR (disableRetryAfterHeader, opt-out with default-on) and updated the description. It's ready for another review when you have a moment.

Per review feedback from @arkodg: rate-limited 429s now include
Retry-After by default, which is a behavior change for existing users.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Signed-off-by: De Ville Weppenaar <11049609+devilleweppenaar@users.noreply.github.com>

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

ClientTrafficPolicy: option to emit Retry-After header on rate-limit 429 responses

3 participants