Skip to content

feat: Add per-unit response headers for all descriptors via LIMIT_ALL_DESCRIPTORS_HEADERS_ENABLED - #1190

Open
samay-arcana wants to merge 2 commits into
envoyproxy:mainfrom
samay-arcana:samay/ratelimit-headers
Open

feat: Add per-unit response headers for all descriptors via LIMIT_ALL_DESCRIPTORS_HEADERS_ENABLED#1190
samay-arcana wants to merge 2 commits into
envoyproxy:mainfrom
samay-arcana:samay/ratelimit-headers

Conversation

@samay-arcana

@samay-arcana samay-arcana commented Jul 13, 2026

Copy link
Copy Markdown

Summary

When LIMIT_RESPONSE_HEADERS_ENABLED is set, the ratelimit service currently returns response headers (RateLimit-Limit, RateLimit-Remaining, RateLimit-Reset) only for the single descriptor closest to hitting the rate limit. This means when multiple descriptors with different time units (e.g., SECOND and MINUTE) are evaluated, headers for only one unit are returned.

This PR adds a new environment variable LIMIT_ALL_DESCRIPTORS_HEADERS_ENABLED that, when enabled, returns per-unit limit and remaining headers for ALL descriptors that have configured limits.

Example

Request with two descriptors (10 req/s and 1000 req/min):

{
  "domain": "envoy",
  "descriptors": [
    {
      "entries": [{ "key": "remote_address", "value": "10.0.0.1" }],
      "limit": { "requests_per_unit": 10, "unit": "SECOND" }
    },
    {
      "entries": [{ "key": "remote_address", "value": "10.0.0.1" }],
      "limit": { "requests_per_unit": 1000, "unit": "MINUTE" }
    }
  ],
  "hits_addend": 1
}

Before (with LIMIT_RESPONSE_HEADERS_ENABLED=true):
Only headers for the descriptor closest to limit are returned (e.g., only seconds).

After (with LIMIT_ALL_DESCRIPTORS_HEADERS_ENABLED=true):

{
  "overall_code": "OK",
  "statuses": [...],
  "response_headers_to_add": [
    { "key": "ratelimit-limit-seconds", "value": "10" },
    { "key": "ratelimit-remaining-seconds", "value": "9" },
    { "key": "ratelimit-limit-minutes", "value": "1000" },
    { "key": "ratelimit-remaining-minutes", "value": "999" }
  ]
}

Changes

src/settings/settings.go

  • Added RateLimitAllDescriptorsHeadersEnabled setting (LIMIT_ALL_DESCRIPTORS_HEADERS_ENABLED env var, default false)

src/service/ratelimit.go

  • Added allDescriptorsHeadersEnabled field to the service struct
  • Updated SetConfig() to read the new setting on config reload
  • Modified header generation in shouldRateLimitWorker(): when allDescriptorsHeadersEnabled is true, generates per-unit headers for all descriptors instead of the single closest-to-limit descriptor
  • Added unitToHeaderSuffix() helper: maps rate limit units to lowercase plural suffixes (SECOND→"seconds", MINUTE→"minutes", HOUR→"hours", etc.)
  • Added allDescriptorsHeaders() method: iterates all descriptor statuses, skips those without limits, and generates ratelimit-limit-{unit} and ratelimit-remaining-{unit} headers

test/service/ratelimit_test.go

  • TestServiceWithAllDescriptorsHeaders: verifies SECOND + MINUTE descriptors produce 4 per-unit headers
  • TestServiceWithAllDescriptorsHeadersOverLimit: verifies headers are returned even when OVER_LIMIT
  • TestServiceWithAllDescriptorsHeadersSkipsNilLimits: verifies descriptors without configured limits are skipped

Backward Compatibility

  • LIMIT_ALL_DESCRIPTORS_HEADERS_ENABLED defaults to false — no behavior change unless explicitly enabled
  • When disabled, the existing LIMIT_RESPONSE_HEADERS_ENABLED behavior is fully preserved
  • LIMIT_ALL_DESCRIPTORS_HEADERS_ENABLED takes precedence over LIMIT_RESPONSE_HEADERS_ENABLED when both are set

Signed-off-by: Samay Varshney samay.varshney@arcana.io

Signed-off-by: Samay Varshney <samay.varshney@arcana.io>
@samay-arcana
samay-arcana force-pushed the samay/ratelimit-headers branch from cc4cc7b to 26dab87 Compare July 13, 2026 19:29
@samay-arcana samay-arcana changed the title Return all rate limit headers via env var feat: Add per-unit response headers for all descriptors via LIMIT_ALL_DESCRIPTORS_HEADERS_ENABLED Jul 13, 2026
@samay-arcana

Copy link
Copy Markdown
Author

@agrawroh can you please review this PR?

@joseluisjimenez1

joseluisjimenez1 commented Aug 7, 2026

Copy link
Copy Markdown

I'm really interested into this, I was just about start working on a PR but find this one 🚀

Comment thread src/service/ratelimit.go Outdated
@joseluisjimenez1

Copy link
Copy Markdown

Hey @samay-arcana LGTM, but let's try to ping some maintainer as I'm not, and not really aware of this codebase.

@samay-arcana

Copy link
Copy Markdown
Author

@collin-lee pls review

@collin-lee

Copy link
Copy Markdown
Contributor

Should we also update README.md to mention this feature/changes?

Comment thread src/service/ratelimit.go
if this.allDescriptorsHeadersEnabled {
response.ResponseHeadersToAdd = this.allDescriptorsHeaders(responseDescriptorStatuses)
// Also include the standard min-descriptor headers for backwards compatibility
if this.customHeadersEnabled && minimumDescriptor != nil {

@collin-lee collin-lee Aug 17, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

When both LIMIT_ALL_DESCRIPTORS_HEADERS_ENABLED and LIMIT_RESPONSE_HEADERS_ENABLED are enabled this block and line 273 will both be emitted twice. I think this block is redundant as lines 273-276 already does this. Maybe try a fourth test enabling both flags.

{Code: pb.RateLimitResponse_OK, CurrentLimit: limits[0].Limit, LimitRemaining: 9},
{Code: pb.RateLimitResponse_OK, CurrentLimit: limits[1].Limit, LimitRemaining: 999},
},
ResponseHeadersToAdd: []*core.HeaderValue{

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The casing here is different than the title-case (RateLimit-Limit) emitted elsewhere

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.

3 participants