Skip to content

Populate request headers in shouldRateLimit rpc response#1168

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

Populate request headers in shouldRateLimit rpc response#1168
yusofg2 wants to merge 29 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.

jpereiramp and others added 28 commits June 18, 2026 10:47
…oyproxy#1111)

Signed-off by: João Pereira <joao@jpereira.me>

Upgrades the gRPC dependency from v1.74.2 to v1.80.0, along with its
transitive dependency updates (golang.org/x/net, google.golang.org/protobuf,
genproto, go-control-plane, etc.).

Signed-off-by: João Pereira <joao.pereira@zwift.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Yusof Ganji <yganji@salesforce.com>
Signed-off-by: Yan Avlasov <yavlasov@google.com>
Signed-off-by: Yusof Ganji <yganji@salesforce.com>
Make the docker image easier to consume.

Signed-off-by: Ian Kerins <git@isk.haus>
Signed-off-by: Yusof Ganji <yganji@salesforce.com>
Signed-off-by: Yusof Ganji <yganji@salesforce.com>
Signed-off-by: Yusof Ganji <yganji@salesforce.com>
…oxy#1124)

Signed-off-by: collin-lee <collin.lee@salesforce.com>
Co-authored-by: collin-lee <collin.lee@salesforce.com>
Signed-off-by: Yusof Ganji <yganji@salesforce.com>
PR envoyproxy#1124 updated the golang base image from 1.26.1 to 1.26.2, but the
new digest sha256:7095ad02810845fa35d1fb090b8e57dd20dce4ca36b29b42951
802350d2ec90e is a single-arch (linux/amd64) image manifest rather
than a multi-arch index. The previous 1.26.1 digest sha256:e2ddb153f7
86ee6210bf8c40f7f35490b3ff7d38be70d1a0d358ba64225f6428 is an OCI image
index covering linux/amd64, arm64/v8, arm/v7, 386, ppc64le, riscv64,
s390x and windows/amd64.

When buildx is asked to produce a non-amd64 variant of the published
envoyproxy/ratelimit image, the FROM line resolves to the amd64 base
on every platform, so the resulting binary is amd64 regardless of the
target. The multi-arch publish then stamps that amd64 binary into the
arm64 layer of the released index, producing an image that fails on
arm64 nodes with:

  exec /bin/ratelimit: exec format error

Swap to the corresponding multi-arch index digest sha256:b54cbf583d39
0341599d7bcbc062425c081105cc5ef6d170ced98ef9d047c716, which contains
the existing 7095ad02... amd64 manifest as one of its children plus
the arm64/v8 and other platform variants. The amd64 image is
unchanged; arm64 builds now produce arm64 binaries.

Signed-off-by: Harrison Harris <harrison.harris@xapien.com>
Co-authored-by: Harrison Harris <harrison.harris@xapien.com>
Signed-off-by: Yusof Ganji <yganji@salesforce.com>
)

Signed-off-by: Yan Avlasov <yavlasov@google.com>
Signed-off-by: Yusof Ganji <yganji@salesforce.com>
)

* feat: add retry in init phase instead of panic directly

Signed-off-by: zirain <zirain2009@gmail.com>

* respect signal handling for graceful shutdown

Signed-off-by: zirain <zirain2009@gmail.com>

* fix test

Signed-off-by: zirain <zirain2009@gmail.com>

---------

Signed-off-by: zirain <zirain2009@gmail.com>
Signed-off-by: Yusof Ganji <yganji@salesforce.com>
Signed-off-by: Yusof Ganji <yganji@salesforce.com>
Co-authored-by: collin-lee <collin.lee@salesforce.com>
Signed-off-by: Yusof Ganji <yganji@salesforce.com>
…yproxy#1150)

Signed-off-by: Immanuel Tikhonov <pchpr.00@list.ru>
Signed-off-by: immanuwell <pchpr.00@list.ru>
Signed-off-by: Yusof Ganji <yganji@salesforce.com>
* redis: bound cluster pipeline parallelism

Signed-off-by: dthuynh <dthuynh@axon.com>

* Refactor to address comment: use gRPC request context in PipeDo, cap the parallelism to RedisPoolSize

Signed-off-by: dthuynh <dthuynh@axon.com>

---------

Signed-off-by: dthuynh <dthuynh@axon.com>
Co-authored-by: dthuynh <dthuynh@axon.com>
Signed-off-by: Yusof Ganji <yganji@salesforce.com>
…roxy#1154)

Signed-off-by: Fred Dafunk <bloomenergyguy@gmail.com>

Co-authored-by: collin-lee <collin.lee@salesforce.com>
Signed-off-by: Yusof Ganji <yganji@salesforce.com>
Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
Signed-off-by: Yusof Ganji <yganji@salesforce.com>
Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
Signed-off-by: Yusof Ganji <yganji@salesforce.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>
Signed-off-by: Yusof Ganji <yganji@salesforce.com>
Signed-off-by: Yusof Ganji <yganji@salesforce.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>
Signed-off-by: Yusof Ganji <yganji@salesforce.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.

Signed-off-by: Yusof Ganji <yganji@salesforce.com>
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>
Signed-off-by: Yusof Ganji <yganji@salesforce.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>
Signed-off-by: Yusof Ganji <yganji@salesforce.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.

Signed-off-by: Yusof Ganji <yganji@salesforce.com>
Signed-off-by: Yusof Ganji <yganji@salesforce.com>
Mirrors the existing rateLimitLimitHeader/rateLimitRemainingHeader/rateLimitResetHeader
pattern used for ResponseHeadersToAdd.

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
Signed-off-by: Yusof Ganji <yganji@salesforce.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>
Signed-off-by: Yusof Ganji <yganji@salesforce.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>
Signed-off-by: Yusof Ganji <yganji@salesforce.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>
Signed-off-by: Yusof Ganji <yganji@salesforce.com>
Signed-off-by: Yusof Ganji <yusofg2@users.noreply.github.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.