Skip to content

Populate request headers in shouldRateLimit rpc response#1168

Open
yusofg2 wants to merge 15 commits into
envoyproxy:mainfrom
yusofg2:main
Open

Populate request headers in shouldRateLimit rpc response#1168
yusofg2 wants to merge 15 commits into
envoyproxy:mainfrom
yusofg2:main

Conversation

@yusofg2

@yusofg2 yusofg2 commented Jun 16, 2026

Copy link
Copy Markdown

Problem

The service that uses global rate limits does not have visibility into rate limiting buckets. Some services require the knowledge of rate limit quota usage.

Use-case

A service sets up rate limits in shadow mode and needs to serve requests that are over limit differently than regularly admitted requests.

Solution

The shouldRateLimit API definition already includes request_headers_to_add in the response to be forwarded to the upstream service but the ratelimit service does not populate the field. We implement population of those headers very similar to the existing response_headers_to_add.
The new configuration parameters are listed in README.md while the design specification and implementation plan are included under docs/superpowers.
In the implementation, a minor bug related to config reload when response headers are disabled was found and fixed in commit 85fb09a.

Testing

For local integration testing:

  1. RLS is configured with LIMIT_REQUEST_HEADERS_ENABLED enabled.
  2. envoy-mock service that acts as the upstream is configured to log the rate limit request headers.
  3. Verified requests sent by integration tests are injected with the rate limit request headers by the envoy sidecar and received and logged by envoy-mock, e.g. run
docker logs integration-test-envoy-mock-1 2>&1 | grep "mock-upstream"

Here is a sample output:

     [mock-upstream] method=GET path=/multiservice-tokenquota RateLimit-Limit=1 RateLimit-Remaining=1 RateLimit-Reset=9
     [mock-upstream] method=GET path=/multiservice-tokenquota RateLimit-Limit=1 RateLimit-Remaining=0 RateLimit-Reset=9
     [mock-upstream] method=GET path=/multiservice-tokenquota RateLimit-Limit=1 RateLimit-Remaining=1 RateLimit-Reset=9
     [mock-upstream] method=GET path=/quota RateLimit-Limit=1 RateLimit-Remaining=0 RateLimit-Reset=9                                               
     [mock-upstream] method=GET path=/quota RateLimit-Limit=1 RateLimit-Remaining=0 RateLimit-Reset=9
     [mock-upstream] method=GET path=/quota RateLimit-Limit=1 RateLimit-Remaining=0 RateLimit-Reset=9                                                   
     [mock-upstream] method=GET path=/fourheader RateLimit-Limit=2 RateLimit-Remaining=1 RateLimit-Reset=9
     [mock-upstream] method=GET path=/fourheader RateLimit-Limit=2 RateLimit-Remaining=0 RateLimit-Reset=9
     [mock-upstream] method=GET path=/fourheader RateLimit-Limit=2 RateLimit-Remaining=1 RateLimit-Reset=9
     [mock-upstream] method=GET path=/fourheader RateLimit-Limit=2 RateLimit-Remaining=0 RateLimit-Reset=9
     [mock-upstream] method=GET path=/fourheader RateLimit-Limit=1 RateLimit-Remaining=0 RateLimit-Reset=9
     [mock-upstream] method=GET path=/fiveheader RateLimit-Limit=4 RateLimit-Remaining=3 RateLimit-Reset=9
     [mock-upstream] method=GET path=/fiveheader RateLimit-Limit=4 RateLimit-Remaining=2 RateLimit-Reset=9
     [mock-upstream] method=GET path=/fiveheader RateLimit-Limit=4 RateLimit-Remaining=1 RateLimit-Reset=9
     [mock-upstream] method=GET path=/fourheader RateLimit-Limit=2 RateLimit-Remaining=1 RateLimit-Reset=9
     [mock-upstream] method=GET path=/fourheader RateLimit-Limit=2 RateLimit-Remaining=0 RateLimit-Reset=9
     [mock-upstream] method=GET path=/fourheader RateLimit-Limit=1 RateLimit-Remaining=0 RateLimit-Reset=9
     [mock-upstream] method=GET path=/fiveheader RateLimit-Limit=4 RateLimit-Remaining=3 RateLimit-Reset=9
     [mock-upstream] method=GET path=/fiveheader RateLimit-Limit=4 RateLimit-Remaining=2 RateLimit-Reset=9
     [mock-upstream] method=GET path=/fiveheader RateLimit-Limit=4 RateLimit-Remaining=1 RateLimit-Reset=9
     [mock-upstream] method=GET path=/fiveheader RateLimit-Limit=4 RateLimit-Remaining=0 RateLimit-Reset=9
     [mock-upstream] method=GET path=/twoheader RateLimit-Limit=1 RateLimit-Remaining=0 RateLimit-Reset=9
     [mock-upstream] method=GET path=/fourheader RateLimit-Limit=2 RateLimit-Remaining=1 RateLimit-Reset=9
     [mock-upstream] method=GET path=/fourheader RateLimit-Limit=2 RateLimit-Remaining=0 RateLimit-Reset=9
     [mock-upstream] method=GET path=/twoheader RateLimit-Limit=3 RateLimit-Remaining=2 RateLimit-Reset=9
     [mock-upstream] method=GET path=/tokenquota RateLimit-Limit=2 RateLimit-Remaining=1 RateLimit-Reset=9
     [mock-upstream] method=GET path=/tokenquota RateLimit-Limit=2 RateLimit-Remaining=0 RateLimit-Reset=9

TBD

  1. Include Claude superpowers artifacts in the open source PR?
  2. Individual commits are not squashed before to show the development and review process.

yusofg2 and others added 15 commits June 5, 2026 16:24
Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
Adds RateLimitRequestHeadersEnabled and three HeaderRequestRatelimit*
fields to Settings, mirroring the existing response-header block, so
the service can later inject rate-limit headers into upstream requests.

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
Add four requestHeaders* fields to the service struct and populate
them from settings in SetConfig, mirroring the existing customHeaders
pattern for response headers.

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
Assign rlSettings.RateLimitRequestHeadersEnabled unconditionally so that a
hot-reload with LIMIT_REQUEST_HEADERS_ENABLED unset/false properly clears
the flag. Previously the if-only guard meant the field could only ever
transition from false → true, making the disable path a no-op.

Also adds a white-box test (src/service/ratelimit_test.go) that directly
exercises the SetConfig toggle path and a black-box test stub in
test/service/ratelimit_test.go that was superseded by the white-box test.
When LIMIT_REQUEST_HEADERS_ENABLED is true, attach RateLimit-Limit,
RateLimit-Remaining, and RateLimit-Reset as RequestHeadersToAdd on the
response, mirroring the existing ResponseHeadersToAdd logic.

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
Add 3 test functions after TestServiceWithDefaultRequestHeaders covering
custom header names, within-limit behaviour, and simultaneous request+response
headers with different names.

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
Assign customHeadersEnabled unconditionally from rlSettings, matching the
existing pattern for requestHeadersEnabled. Previously the field was only
ever set to true inside an if-block, so a hot-reload with
LIMIT_RESPONSE_HEADERS_ENABLED=false had no effect and response headers
continued to be added indefinitely.
Mirrors the existing rateLimitLimitHeader/rateLimitRemainingHeader/rateLimitResetHeader
pattern used for ResponseHeadersToAdd.

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
Relative paths (./examples/...) resolve relative to the compose file
location (integration-test/) rather than the repo root, causing mount
failures. Use ${PWD} consistently, matching the existing ratelimit and
tester service mounts.

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
…ation tests

- Add access log format to envoy-mock that logs RateLimit-* request headers,
  allowing end-to-end verification that Envoy injects request_headers_to_add
  onto the upstream request
- Enable LIMIT_REQUEST_HEADERS_ENABLED in integration test compose

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
- Remove duplicate pb_struct import alias in src/service/ratelimit_test.go;
  use existing ratelimitv3 alias throughout
- Add comment to examples/envoy/mock.yaml noting the hardcoded default header
  names and how to update them if custom LIMIT_REQUEST_*_HEADER env vars are used
- Remove local absolute paths from implementation plan docs

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
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.

1 participant