Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -131,8 +131,14 @@ public GatewayFilter apply(Config config) {
}
return limiter.isAllowed(routeId, key).flatMap(response -> {

for (Map.Entry<String, String> header : response.getHeaders().entrySet()) {
exchange.getResponse().getHeaders().add(header.getKey(), header.getValue());
// The response may already be committed when the filter chain is
// re-subscribed with the same exchange (for example by the retry
// filter). Adding headers to a committed response would throw
// UnsupportedOperationException from ReadOnlyHttpHeaders.
if (!exchange.getResponse().isCommitted()) {
for (Map.Entry<String, String> header : response.getHeaders().entrySet()) {
exchange.getResponse().getHeaders().add(header.getKey(), header.getValue());
}
}

if (response.isAllowed()) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,36 @@ public void emptyKeyDeniedWithThrowOnLimit() {
assertFilterFactory(exchange -> Mono.empty(), null, true, HttpStatus.FORBIDDEN, true, true);
}

@Test
public void deniedDoesNotMutateHeadersWhenResponseAlreadyCommitted() {
// gh-4175: when the filter chain is re-subscribed with the same exchange (for
// example by the retry filter), the response may already be committed. Adding the
// rate-limit headers to a committed response throws UnsupportedOperationException
// (ReadOnlyHttpHeaders), so the filter must skip the header mutation.
String key = "notallowedkey";
Map<String, String> headers = Collections.singletonMap("X-Tokens-Remaining", "0");
when(rateLimiter.isAllowed("myroute", key)).thenReturn(Mono.just(new Response(false, headers)));

MockServerHttpRequest request = MockServerHttpRequest.get("/").build();
MockServerWebExchange exchange = MockServerWebExchange.from(request);
exchange.getAttributes()
.put(ServerWebExchangeUtils.GATEWAY_ROUTE_ATTR,
Route.async().id("myroute").predicate(ex -> true).uri("http://localhost").build());

// Simulate a previous subscription having already committed the response.
exchange.getResponse().setComplete().block();
assertThat(exchange.getResponse().isCommitted()).isTrue();

RequestRateLimiterGatewayFilterFactory factory = this.context
.getBean(RequestRateLimiterGatewayFilterFactory.class);
GatewayFilter filter = factory.apply(config -> {
config.setRouteId("myroute");
config.setKeyResolver(resolver2);
});

StepVerifier.create(filter.filter(exchange, this.filterChain)).expectComplete().verify();
}

private void assertFilterFactory(KeyResolver keyResolver, String key, boolean allowed, HttpStatus expectedStatus) {
assertFilterFactory(keyResolver, key, allowed, expectedStatus, null, false);
}
Expand Down