spring-cloud / spring-cloud/spring-cloud-gateway
RequestRateLimiterGatewayFilterFactory commits 429 response instead of throwing
Nobody has claimed this yet.
- Dominant language
- Java
- Stars
- 4.9k
- Forks
- 3.5k
- Avg merge
- 20h 57m
- Merged PRs (30d)
- 8
Description
Describe the bug
RequestRateLimiterGatewayFilterFactory calls exchange.getResponse().setComplete() when the rate limit is exceeded.
This commits the response immediately, which has two negative side-effects:
-
Retry breaks with
UnsupportedOperationException: ReadOnlyHttpHeaders.
WhenRetryGatewayFilterFactoryis placed before the rate limiter and configured withseries=CLIENT_ERROR, the retry operator sees the committed429status and re-executes the filter chain using the sameServerWebExchange. On the second pass,RequestRateLimiterGatewayFilterFactoryattempts to addX-RateLimit-*headers to the already-committed response, causing an immediateUnsupportedOperationExceptionfromReadOnlyHttpHeaders. -
Custom exception handlers cannot intercept 429.
AnyWebExceptionHandlerorErrorWebExceptionHandlerbean is bypassed because the filter returns a successfulMono<Void>rather than propagating an error.
Regression / Prior art
This was raised in #575 (2018) where @spencergibb suggested the fix:
"I think rather than setting a status we can use
ResponseStatusExceptioninMono.error."
That change was never applied.
Versions
- Spring Cloud Gateway Server: 4.2.7
- Spring Boot: 3.5.14
- Java: 25
Minimal reproduction
Route configuration
spring:
cloud:
gateway:
routes:
- id: ratelimit-retry-bug
uri: http://localhost:8081
predicates:
- Path=/test/**
filters:
# Retry is placed BEFORE rate limiter
- name: Retry
args:
retries: 3
series: CLIENT_ERROR
- name: RequestRateLimiter
args:
redis-rate-limiter.replenishRate: 1
redis-rate-limiter.burstCapacity: 1
redis-rate-limiter.requestedTokens: 1
Steps
─────
1. Send a request that exceeds the rate limit.
2. Observe that CustomExceptionHandler is not invoked.
3. If Retry is present with series=CLIENT_ERROR, the application throws:
java.lang.UnsupportedOperationException: null
at org.springframework.http.ReadOnlyHttpHeaders.add(ReadOnlyHttpHeaders.java:97)
at org.springframework.cloud.gateway.filter.factory.RequestRateLimiterGatewayFilterFactory.lambda$apply$1(RequestRateLimiterGatewayFilterFactory.java:
112)
Expected behavior
─────────────────
When allowed=false, the filter should throw rather than commit the response, e.g.:
return Mono.error(new ResponseStatusException(config.getStatusCode(), "Rate limit exceeded" ));
This allows:
• ErrorWebExceptionHandler / WebExceptionHandler to customize the 429 response.
• RetryGatewayFilterFactory to decide whether to retry based on the exception (e.g. do NOT retry on ResponseStatusException), avoiding the ReadOnlyHttpHears crash.
Actual behavior
───────────────
The filter calls setResponseStatus(exchange, config.getStatusCode()) followed by exchange.getResponse().setComplete(), which commits the response and returns a successful Mono<Void>.
Suggested fix
─────────────
Replace the final setComplete() branch in RequestRateLimiterGatewayFilterFactory.apply() with Mono.error(new ResponseStatusException(...)), consistent with the suggestion in #575.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
The affected entry point is RequestRateLimiterGatewayFilterFactory.apply(), specifically the allowed=false branch described in the issue. Read the existing response-status/setComplete path and the RetryGatewayFilterFactory interaction, then verify with the supplied retry reproduction that 429 handling reaches exception handlers without reusing a committed response.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- java, spring, spring-boot
- Domain
- api, backend
- Issue type
- Bug
- Difficulty
- 2/5
- Estimated time
- 1-3 hours
- Activity status
- Quiet
- Clarity
- Clearly specified
- Newbie friendliness
- 68/100