Open blacelle opened 4 years ago
let us know when you have something that reproduces it
@spencergibb I'm not proficient enough to provide rock-solid feedback, sorry. In the path to write a minimal reproducing case, I ended on a test failing for an unknown reason (it seem to process the redirection as fallbackUri, but get stuck right there ; in the project (which of-course is more complex, and not shareable as it is), the dedupHeaders fails a bit after reference to the 302):
2020-07-08 20:52:04,345 DEBUG[main] o.s.w.r.f.client.ExchangeFunctions|traceDebug(91) - [600b3bee] HTTP GET http://localhost:54436/someRoute
2020-07-08 20:52:04,441 DEBUG[reactor-http-nio-3] o.s.w.s.a.HttpWebHandlerAdapter|traceDebug(91) - [e506334a-1] HTTP GET "/someRoute"
2020-07-08 20:52:04,454 DEBUG[reactor-http-nio-3] o.s.c.g.h.RoutePredicateHandlerMapping|lambda$lookupRoute$6(149) - Route matched: 4d3c4745-0e66-4f60-ae8e-db6c7e7cc329
2020-07-08 20:52:04,455 DEBUG[reactor-http-nio-3] o.s.c.g.h.RoutePredicateHandlerMapping|lambda$getHandlerInternal$0(92) - Mapping [Exchange: GET http://localhost:54436/someRoute] to Route{id='4d3c4745-0e66-4f60-ae8e-db6c7e7cc329', uri=https://example.com:443, order=0, predicate=Paths: [/someRoute/**], match trailing slash: true, gatewayFilters=[[[DedupeResponseHeader Date, header1, header2 = RETAIN_LAST], order = 0], [[SpringCloudCircuitBreakerResilience4JFilterFactory name = 'circuitBreakerId', fallback = forward:/maintenance], order = 0]], metadata={}}
2020-07-08 20:52:04,455 DEBUG[reactor-http-nio-3] o.s.c.g.h.RoutePredicateHandlerMapping|lambda$getHandler$1(183) - [e506334a-1] Mapped to org.springframework.cloud.gateway.handler.FilteringWebHandler@58f0ddab
2020-07-08 20:52:04,455 DEBUG[reactor-http-nio-3] o.s.c.g.handler.FilteringWebHandler|handle(85) - Sorted gatewayFilterFactories: [[GatewayFilterAdapter{delegate=org.springframework.cloud.gateway.filter.RemoveCachedBodyFilter@35cbeb54}, order = -2147483648], [GatewayFilterAdapter{delegate=org.springframework.cloud.gateway.filter.AdaptCachedBodyGlobalFilter@5a2f401a}, order = -2147482648], [GatewayFilterAdapter{delegate=org.springframework.cloud.gateway.filter.NettyWriteResponseFilter@e2c627e}, order = -1], [GatewayFilterAdapter{delegate=org.springframework.cloud.gateway.filter.ForwardPathFilter@335972a5}, order = 0], [GatewayFilterAdapter{delegate=org.springframework.cloud.gateway.filter.GatewayMetricsFilter@49f2646}, order = 0], [[DedupeResponseHeader Date, header1, header2 = RETAIN_LAST], order = 0], [[SpringCloudCircuitBreakerResilience4JFilterFactory name = 'circuitBreakerId', fallback = forward:/maintenance], order = 0], [GatewayFilterAdapter{delegate=org.springframework.cloud.gateway.filter.RouteToRequestUrlFilter@a7ae340}, order = 10000], [GatewayFilterAdapter{delegate=org.springframework.cloud.gateway.config.GatewayNoLoadBalancerClientAutoConfiguration$NoLoadBalancerClientFilter@4b00d59}, order = 10100], [GatewayFilterAdapter{delegate=org.springframework.cloud.gateway.filter.WebsocketRoutingFilter@59a2bed1}, order = 2147483646], GatewayFilterAdapter{delegate=io.mitrust_bug_report.dedupcircuitbreaker.BugReportApplication$$Lambda$702/0x0000000800692040@644b68ad}, [GatewayFilterAdapter{delegate=org.springframework.cloud.gateway.filter.NettyRoutingFilter@1939a394}, order = 2147483647], [GatewayFilterAdapter{delegate=org.springframework.cloud.gateway.filter.ForwardRoutingFilter@5e8bd498}, order = 2147483647]]
2020-07-08 20:52:05,029 INFO [reactor-http-nio-3] i.m.d.BugReportApplication|lambda$9(158) - Event circuit_breaker_name=circuitBreakerId event_type=ERROR
2020-07-08 20:52:05,035 DEBUG[reactor-http-nio-3] o.s.w.r.r.m.a.RequestMappingHandlerMapping|lambda$getHandler$1(183) - [e506334a-1] Mapped to io.mitrust_bug_report.dedupcircuitbreaker.BugReportApplication$BugReportController#getMaintenanceMode()
2020-07-08 20:52:05,054 DEBUG[reactor-http-nio-3] o.s.w.s.a.HttpWebHandlerAdapter|traceDebug(91) - [e506334a-1] Completed 302 FOUND
2020-07-08 20:52:05,067 DEBUG[reactor-http-nio-2] o.s.w.r.f.client.ExchangeFunctions|traceDebug(91) - [600b3bee] Response 302 FOUND
Then nothing for 5 seconds.
But the test fails with:
java.lang.IllegalStateException: Timeout on blocking read for 5000 MILLISECONDS
at reactor.core.publisher.BlockingSingleSubscriber.blockingGet(BlockingSingleSubscriber.java:123)
at reactor.core.publisher.Mono.block(Mono.java:1703)
at org.springframework.test.web.reactive.server.ExchangeResult.formatBody(ExchangeResult.java:252)
at org.springframework.test.web.reactive.server.ExchangeResult.toString(ExchangeResult.java:225)
at java.base/java.lang.String.valueOf(String.java:2951)
at java.base/java.lang.StringBuilder.append(StringBuilder.java:168)
at org.springframework.test.web.reactive.server.ExchangeResult.assertWithDiagnostics(ExchangeResult.java:209)
at org.springframework.test.web.reactive.server.StatusAssertions.isEqualTo(StatusAssertions.java:59)
at org.springframework.test.web.reactive.server.StatusAssertions.isEqualTo(StatusAssertions.java:51)
at io.mitrust_bug_report.dedupcircuitbreaker.TestSpringCloudIssueDedupCircuitBreaker.test_issue_dedup(TestSpringCloudIssueDedupCircuitBreaker.java:39)
Given by:
package io.mitrust_bug_report.dedupcircuitbreaker;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.cloud.contract.wiremock.AutoConfigureWireMock;
import org.springframework.http.HttpStatus;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.reactive.server.WebTestClient;
import com.github.tomakehurst.wiremock.client.WireMock;
@RunWith(SpringRunner.class)
@AutoConfigureWireMock(port = 0)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class TestSpringCloudIssueDedupCircuitBreaker {
@Autowired
private WebTestClient webClient;
@Before
public void setup() {
// Wiremock stubs Datasharing and RetrieverProxy
WireMock.stubFor(WireMock.get(WireMock.anyUrl())
.willReturn(WireMock.aResponse().withStatus(HttpStatus.BAD_GATEWAY.value())));
}
// https://github.com/spring-cloud/spring-cloud-gateway/issues/1822
@Test
public void test_issue_dedup() {
webClient.get().uri("someRoute").exchange().expectStatus().isEqualTo(HttpStatus.INTERNAL_SERVER_ERROR);
}
}
package io.mitrust_bug_report.dedupcircuitbreaker;
import java.util.Optional;
import java.util.function.Function;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.actuate.autoconfigure.security.reactive.ReactiveManagementWebSecurityAutoConfiguration;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
import org.springframework.boot.autoconfigure.security.reactive.ReactiveSecurityAutoConfiguration;
import org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration;
import org.springframework.cloud.circuitbreaker.resilience4j.ReactiveResilience4JCircuitBreakerFactory;
import org.springframework.cloud.client.circuitbreaker.Customizer;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.cloud.gateway.filter.factory.DedupeResponseHeaderGatewayFilterFactory.Strategy;
import org.springframework.cloud.gateway.route.Route;
import org.springframework.cloud.gateway.route.RouteLocator;
import org.springframework.cloud.gateway.route.builder.GatewayFilterSpec;
import org.springframework.cloud.gateway.route.builder.PredicateSpec;
import org.springframework.cloud.gateway.route.builder.RouteLocatorBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PatchMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.server.ResponseStatusException;
import io.github.resilience4j.circuitbreaker.CircuitBreakerConfig;
import io.github.resilience4j.circuitbreaker.event.CircuitBreakerEvent;
import io.github.resilience4j.core.EventConsumer;
import io.github.resilience4j.timelimiter.TimeLimiterConfig;
import reactor.core.publisher.Mono;
@SpringBootApplication(exclude = { DataSourceAutoConfiguration.class,
SecurityAutoConfiguration.class,
ReactiveSecurityAutoConfiguration.class,
ReactiveManagementWebSecurityAutoConfiguration.class, })
public class BugReportApplication {
private static final Logger LOGGER = LoggerFactory.getLogger(BugReportApplication.class);
protected BugReportApplication() {
// hidden
}
@RestController
public static class BugReportController {
// The route to which Gateway redirect to maintenance behavior (typically redirected from CircuitBreaker)
public static final String ROUTE_MAINTENANCE = "/maintenance";
// The route showing maintenance screen to the user
private static final String ROUTE_STATIC_MAINTENANCE_HTML = "/static-maintenance.html";
/**
* Forward GET (and others, else '405 METHOD_NOT_ALLOWED') from circuitbreaker (redirect doesn't work)
*
* @return
*/
@GetMapping(BugReportController.ROUTE_MAINTENANCE)
@PostMapping(BugReportController.ROUTE_MAINTENANCE)
@PatchMapping(BugReportController.ROUTE_MAINTENANCE)
@PutMapping(BugReportController.ROUTE_MAINTENANCE)
@DeleteMapping(BugReportController.ROUTE_MAINTENANCE)
public ResponseEntity<Object> getMaintenanceMode() {
HttpHeaders headers = new HttpHeaders();
headers.add(HttpHeaders.LOCATION, ROUTE_STATIC_MAINTENANCE_HTML);
return new ResponseEntity<>(headers, HttpStatus.FOUND);
}
}
@Bean
public RouteLocator routeLocator(RouteLocatorBuilder builder) {
return builder.routes().route(someRerouting()).build();
}
private Function<PredicateSpec, Route.AsyncBuilder> someRerouting() {
return reroute("/someRoute", null, "https://example.com");
}
private Function<PredicateSpec, Route.AsyncBuilder> reroute(String path, String rewritePath, String uri) {
return r -> r.path(path + "/**").filters(f -> {
GatewayFilterSpec filterSpec = Optional.ofNullable(rewritePath)
.map(s -> f.rewritePath(s + "/(?<segment>.*)", "/${segment}"))
.orElse(f)
.dedupeResponseHeader("Date, header1, header2", Strategy.RETAIN_LAST.name());
filterSpec = filterSpec.circuitBreaker(c -> c.setName("circuitBreakerId")
// forward, because redirect doesn't work
.setFallbackUri("forward:" + BugReportController.ROUTE_MAINTENANCE));
return filterSpec;
}).uri(uri);
}
// https://cloud.spring.io/spring-cloud-gateway/multi/multi__global_filters.html
// https://mromeh.com/2020/05/25/spring-cloud-gateway-with-resilience4j-circuit-breaker-part-2/
@Bean
public GlobalFilter httpStatusFilter() {
return (exchange, chain) -> chain.filter(exchange).then(Mono.defer(() -> {
HttpStatus statusCode = exchange.getResponse().getStatusCode();
// THIS CODE IS WRONG: WE ARE CHECKING HERE THE STATUS_CODE FROM GATEWAY, AND NOT FOR THE PROXIED SERVICE
if (statusCode.is4xxClientError() || statusCode.is5xxServerError()) {
if (!exchange.getResponse().isCommitted()) {
return Mono.error(new ResponseStatusException(statusCode));
} else {
LOGGER.warn("Already committed. Not turning into an Exception");
}
}
return Mono.empty();
}));
}
/**
* if 50% of 5 consecutive requests gone in timeout (5s), the circuit open <br>
* stay 10s open <br>
* then half-open for only 5 requests, if 50% of these requests failed, it opens again, else it closes <br>
*
* all failed requests are redirected to '/static-maintenance.html'
*
* @return customizer for circuitbreaker
*
* @see <a href= "https://resilience4j.readme.io/docs/circuitbreaker">resilience4j doc</a>
* @see <a href=
* "https://piotrminkowski.com/2019/12/11/circuit-breaking-in-spring-cloud-gateway-with-resilience4j/">gateway
* + resilience4j</a>
*/
@Bean
public Customizer<ReactiveResilience4JCircuitBreakerFactory> defaultCustomizer() {
// Open Circuit on HttpStatus
// https://github.com/spring-cloud/spring-cloud-gateway/issues/1735
// https://cloud.spring.io/spring-cloud-circuitbreaker/reference/html/index.html#reactive-example-2
return factory -> {
factory.configure(
builder -> builder.timeLimiterConfig(TimeLimiterConfig.ofDefaults())
.circuitBreakerConfig(CircuitBreakerConfig.ofDefaults()),
"circuitBreakerId");
factory.addCircuitBreakerCustomizer(
circuitBreaker -> circuitBreaker.getEventPublisher().onEvent(makeCircuitBreakerEventListener()),
"circuitBreakerId");
};
}
private EventConsumer<CircuitBreakerEvent> makeCircuitBreakerEventListener() {
return event -> {
// We log all events, in the hope to understand which events are interesting
LOGGER.info("Event circuit_breaker_name={} event_type={}",
event.getCircuitBreakerName(),
event.getEventType());
};
}
}
relying on spring-cloud-starter.gateway:2.2.3
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
<exclusions>
<exclusion>
<!-- No need for the security in the Gateway -->
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-rsa</artifactId>
</exclusion>
<exclusion>
<!-- No need for the security in the Gateway -->
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-crypto</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-config</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-circuitbreaker-reactor-resilience4j</artifactId>
</dependency>
<dependency>
<!-- The static files of the Vue app -->
<groupId>io.mitrust</groupId>
<artifactId>web</artifactId>
</dependency>
<dependency>
<groupId>io.mitrust</groupId>
<artifactId>webapp</artifactId>
<exclusions>
<!-- Not compatible with WebFlux -->
<exclusion>
<artifactId>spring-boot-starter-web</artifactId>
<groupId>org.springframework.boot</groupId>
</exclusion>
<exclusion>
<artifactId>spring-webmvc</artifactId>
<groupId>org.springframework</groupId>
</exclusion>
<exclusion>
<!-- No need for the security in the Gateway -->
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-crypto</artifactId>
</exclusion>
</exclusions>
</dependency>
<!-- <dependency> -->
<!-- <groupId>org.springframework.boot</groupId> -->
<!-- <artifactId>spring-boot-devtools</artifactId> -->
<!-- <optional>true</optional> -->
<!-- </dependency> -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.hibernate.validator</groupId>
<artifactId>hibernate-validator</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-contract-wiremock</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<!-- TestReactiveTechnicalDetailsProvider need it ?! -->
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.awaitility</groupId>
<artifactId>awaitility</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
Hi,
Having same issues with the dedupe filter. My scenario is as follows: in one of my GlobalFilters I set the exchange as already routed and return a default response. Also, I have a dedupe filter which will throw an exception if there are doubled headers.
I've attached the demo project demo.zip
application.yml file
spring:
cloud:
gateway:
default-filters:
- DedupeResponseHeader=TestHeader, RETAIN_LAST
routes:
- id: httpbin
uri: https://httpbin.org
predicates:
- Path=/service/httpbin/**
filters:
- StripPrefix=2
The global filter that returns a default body
@Component
public class DefaultResponseTestFilter implements GlobalFilter, Ordered {
@Override
public Mono<Void> filter (ServerWebExchange exchange, GatewayFilterChain chain) {
ServerWebExchangeUtils.setAlreadyRouted(exchange);
exchange.getResponse().getHeaders().add("TestHeader", "value1");
exchange.getResponse().getHeaders().add("TestHeader", "value2");
return chain.filter(exchange).then(Mono.defer(() -> {
exchange.getResponse().getHeaders().set(CONTENT_TYPE, "application/json");
DataBuffer buffer = exchange.getResponse().bufferFactory().wrap("{\"test\":\"YYY\"}".getBytes());
return exchange.getResponse().writeWith(Flux.just(buffer));
}));
}
@Override
public int getOrder () {
return 2;
}
}
And the stacktrace:
2021-03-26 13:21:54.144 ERROR 17224 --- [ctor-http-nio-2] o.s.w.s.adapter.HttpWebHandlerAdapter : [17552e31-1] Error [java.lang.UnsupportedOperationException] for HTTP GET "/service/httpbin", but ServerHttpResponse already committed (200 OK)
2021-03-26 13:21:54.148 ERROR 17224 --- [ctor-http-nio-2] r.n.http.server.HttpServerOperations : [id:17552e31-1, L:/0:0:0:0:0:0:0:1:8080 - R:/0:0:0:0:0:0:0:1:63058] Error finishing response. Closing connection
java.lang.UnsupportedOperationException: null
at org.springframework.http.ReadOnlyHttpHeaders.set(ReadOnlyHttpHeaders.java:106) ~[spring-web-5.3.5.jar:5.3.5]
Suppressed: reactor.core.publisher.FluxOnAssembly$OnAssemblyException:
Error has been observed at the following site(s):
|_ checkpoint ⇢ org.springframework.cloud.gateway.filter.WeightCalculatorWebFilter [DefaultWebFilterChain]
|_ checkpoint ⇢ HTTP GET "/service/httpbin" [ExceptionHandlingWebHandler]
Stack trace:
at org.springframework.http.ReadOnlyHttpHeaders.set(ReadOnlyHttpHeaders.java:106) ~[spring-web-5.3.5.jar:5.3.5]
at org.springframework.cloud.gateway.filter.factory.DedupeResponseHeaderGatewayFilterFactory.dedupe(DedupeResponseHeaderGatewayFilterFactory.java:143) ~[spring-cloud-gateway-server-3.0.2.jar:3.0.2]
at org.springframework.cloud.gateway.filter.factory.DedupeResponseHeaderGatewayFilterFactory.dedupe(DedupeResponseHeaderGatewayFilterFactory.java:129) ~[spring-cloud-gateway-server-3.0.2.jar:3.0.2]
at org.springframework.cloud.gateway.filter.factory.DedupeResponseHeaderGatewayFilterFactory$1.lambda$filter$0(DedupeResponseHeaderGatewayFilterFactory.java:92) ~[spring-cloud-gateway-server-3.0.2.jar:3.0.2]
at reactor.core.publisher.MonoRunnable.call(MonoRunnable.java:73) ~[reactor-core-3.4.4.jar:3.4.4]
at reactor.core.publisher.MonoRunnable.call(MonoRunnable.java:32) ~[reactor-core-3.4.4.jar:3.4.4]
at reactor.core.publisher.MonoIgnoreThen$ThenIgnoreMain.drain(MonoIgnoreThen.java:136) ~[reactor-core-3.4.4.jar:3.4.4]
at reactor.core.publisher.MonoIgnoreThen$ThenIgnoreMain.ignoreDone(MonoIgnoreThen.java:191) ~[reactor-core-3.4.4.jar:3.4.4]
at reactor.core.publisher.MonoIgnoreThen$ThenIgnoreInner.onComplete(MonoIgnoreThen.java:248) ~[reactor-core-3.4.4.jar:3.4.4]
at reactor.core.publisher.Operators$MonoSubscriber.onComplete(Operators.java:1857) ~[reactor-core-3.4.4.jar:3.4.4]
at reactor.core.publisher.MonoIgnoreThen$ThenAcceptInner.onComplete(MonoIgnoreThen.java:323) ~[reactor-core-3.4.4.jar:3.4.4]
at reactor.core.publisher.MonoPeekTerminal$MonoTerminalPeekSubscriber.onComplete(MonoPeekTerminal.java:299) ~[reactor-core-3.4.4.jar:3.4.4]
at org.springframework.http.server.reactive.ChannelSendOperator$WriteCompletionBarrier.onComplete(ChannelSendOperator.java:423) ~[spring-web-5.3.5.jar:5.3.5]
at reactor.core.publisher.MonoIgnoreElements$IgnoreElementsSubscriber.onComplete(MonoIgnoreElements.java:88) ~[reactor-core-3.4.4.jar:3.4.4]
at reactor.core.publisher.FluxConcatArray$ConcatArraySubscriber.onComplete(FluxConcatArray.java:192) ~[reactor-core-3.4.4.jar:3.4.4]
at reactor.core.publisher.Operators$MonoSubscriber.onComplete(Operators.java:1857) ~[reactor-core-3.4.4.jar:3.4.4]
at reactor.core.publisher.MonoIgnoreThen$ThenAcceptInner.onComplete(MonoIgnoreThen.java:323) ~[reactor-core-3.4.4.jar:3.4.4]
at reactor.netty.channel.MonoSendMany$SendManyInner.run(MonoSendMany.java:349) ~[reactor-netty-core-1.0.5.jar:1.0.5]
at reactor.netty.channel.MonoSendMany$SendManyInner.trySchedule(MonoSendMany.java:423) ~[reactor-netty-core-1.0.5.jar:1.0.5]
at reactor.netty.channel.MonoSendMany$SendManyInner.trySuccess(MonoSendMany.java:582) ~[reactor-netty-core-1.0.5.jar:1.0.5]
at reactor.netty.channel.MonoSendMany$SendManyInner.trySuccess(MonoSendMany.java:118) ~[reactor-netty-core-1.0.5.jar:1.0.5]
at io.netty.util.concurrent.PromiseCombiner.tryPromise(PromiseCombiner.java:170) ~[netty-common-4.1.60.Final.jar:4.1.60.Final]
at io.netty.util.concurrent.PromiseCombiner.access$600(PromiseCombiner.java:35) ~[netty-common-4.1.60.Final.jar:4.1.60.Final]
at io.netty.util.concurrent.PromiseCombiner$1.operationComplete0(PromiseCombiner.java:62) ~[netty-common-4.1.60.Final.jar:4.1.60.Final]
at io.netty.util.concurrent.PromiseCombiner$1.operationComplete(PromiseCombiner.java:44) ~[netty-common-4.1.60.Final.jar:4.1.60.Final]
at io.netty.util.concurrent.DefaultPromise.notifyListener0(DefaultPromise.java:578) ~[netty-common-4.1.60.Final.jar:4.1.60.Final]
at io.netty.util.concurrent.DefaultPromise.notifyListenersNow(DefaultPromise.java:552) ~[netty-common-4.1.60.Final.jar:4.1.60.Final]
at io.netty.util.concurrent.DefaultPromise.notifyListeners(DefaultPromise.java:491) ~[netty-common-4.1.60.Final.jar:4.1.60.Final]
at io.netty.util.concurrent.DefaultPromise.setValue0(DefaultPromise.java:616) ~[netty-common-4.1.60.Final.jar:4.1.60.Final]
at io.netty.util.concurrent.DefaultPromise.setSuccess0(DefaultPromise.java:605) ~[netty-common-4.1.60.Final.jar:4.1.60.Final]
at io.netty.util.concurrent.DefaultPromise.trySuccess(DefaultPromise.java:104) ~[netty-common-4.1.60.Final.jar:4.1.60.Final]
at io.netty.util.internal.PromiseNotificationUtil.trySuccess(PromiseNotificationUtil.java:48) ~[netty-common-4.1.60.Final.jar:4.1.60.Final]
at io.netty.channel.ChannelOutboundBuffer.safeSuccess(ChannelOutboundBuffer.java:717) ~[netty-transport-4.1.60.Final.jar:4.1.60.Final]
at io.netty.channel.ChannelOutboundBuffer.remove(ChannelOutboundBuffer.java:272) ~[netty-transport-4.1.60.Final.jar:4.1.60.Final]
at io.netty.channel.ChannelOutboundBuffer.removeBytes(ChannelOutboundBuffer.java:352) ~[netty-transport-4.1.60.Final.jar:4.1.60.Final]
at io.netty.channel.socket.nio.NioSocketChannel.doWrite(NioSocketChannel.java:431) ~[netty-transport-4.1.60.Final.jar:4.1.60.Final]
at io.netty.channel.AbstractChannel$AbstractUnsafe.flush0(AbstractChannel.java:941) ~[netty-transport-4.1.60.Final.jar:4.1.60.Final]
at io.netty.channel.nio.AbstractNioChannel$AbstractNioUnsafe.flush0(AbstractNioChannel.java:354) ~[netty-transport-4.1.60.Final.jar:4.1.60.Final]
at io.netty.channel.AbstractChannel$AbstractUnsafe.flush(AbstractChannel.java:905) ~[netty-transport-4.1.60.Final.jar:4.1.60.Final]
at io.netty.channel.DefaultChannelPipeline$HeadContext.flush(DefaultChannelPipeline.java:1372) ~[netty-transport-4.1.60.Final.jar:4.1.60.Final]
at io.netty.channel.AbstractChannelHandlerContext.invokeFlush0(AbstractChannelHandlerContext.java:750) ~[netty-transport-4.1.60.Final.jar:4.1.60.Final]
at io.netty.channel.AbstractChannelHandlerContext.invokeFlush(AbstractChannelHandlerContext.java:742) ~[netty-transport-4.1.60.Final.jar:4.1.60.Final]
at io.netty.channel.AbstractChannelHandlerContext.flush(AbstractChannelHandlerContext.java:728) ~[netty-transport-4.1.60.Final.jar:4.1.60.Final]
at io.netty.channel.CombinedChannelDuplexHandler$DelegatingChannelHandlerContext.flush(CombinedChannelDuplexHandler.java:531) ~[netty-transport-4.1.60.Final.jar:4.1.60.Final]
at io.netty.channel.ChannelOutboundHandlerAdapter.flush(ChannelOutboundHandlerAdapter.java:125) ~[netty-transport-4.1.60.Final.jar:4.1.60.Final]
at io.netty.channel.CombinedChannelDuplexHandler.flush(CombinedChannelDuplexHandler.java:356) ~[netty-transport-4.1.60.Final.jar:4.1.60.Final]
at io.netty.channel.AbstractChannelHandlerContext.invokeFlush0(AbstractChannelHandlerContext.java:750) ~[netty-transport-4.1.60.Final.jar:4.1.60.Final]
at io.netty.channel.AbstractChannelHandlerContext.invokeFlush(AbstractChannelHandlerContext.java:742) ~[netty-transport-4.1.60.Final.jar:4.1.60.Final]
at io.netty.channel.AbstractChannelHandlerContext.flush(AbstractChannelHandlerContext.java:728) ~[netty-transport-4.1.60.Final.jar:4.1.60.Final]
at reactor.netty.channel.MonoSendMany$SendManyInner$AsyncFlush.run(MonoSendMany.java:778) ~[reactor-netty-core-1.0.5.jar:1.0.5]
at io.netty.util.concurrent.AbstractEventExecutor.safeExecute$$$capture(AbstractEventExecutor.java:164) ~[netty-common-4.1.60.Final.jar:4.1.60.Final]
at io.netty.util.concurrent.AbstractEventExecutor.safeExecute(AbstractEventExecutor.java) ~[netty-common-4.1.60.Final.jar:4.1.60.Final]
at io.netty.util.concurrent.SingleThreadEventExecutor.runAllTasks(SingleThreadEventExecutor.java:472) ~[netty-common-4.1.60.Final.jar:4.1.60.Final]
at io.netty.channel.nio.NioEventLoop.run(NioEventLoop.java:497) ~[netty-transport-4.1.60.Final.jar:4.1.60.Final]
at io.netty.util.concurrent.SingleThreadEventExecutor$4.run(SingleThreadEventExecutor.java:989) ~[netty-common-4.1.60.Final.jar:4.1.60.Final]
at io.netty.util.internal.ThreadExecutorMap$2.run(ThreadExecutorMap.java:74) ~[netty-common-4.1.60.Final.jar:4.1.60.Final]
at io.netty.util.concurrent.FastThreadLocalRunnable.run(FastThreadLocalRunnable.java:30) ~[netty-common-4.1.60.Final.jar:4.1.60.Final]
at java.base/java.lang.Thread.run(Thread.java:834) ~[na:na]
Hi,
Having same issues with the dedupe filter. My scenario is as follows: in one of my GlobalFilters I set the exchange as already routed and return a default response. Also, I have a dedupe filter which will throw an exception if there are doubled headers.
I've attached the demo project demo.zip
application.yml file
spring: cloud: gateway: default-filters: - DedupeResponseHeader=TestHeader, RETAIN_LAST routes: - id: httpbin uri: https://httpbin.org predicates: - Path=/service/httpbin/** filters: - StripPrefix=2
The global filter that returns a default body
@Component public class DefaultResponseTestFilter implements GlobalFilter, Ordered { @Override public Mono<Void> filter (ServerWebExchange exchange, GatewayFilterChain chain) { ServerWebExchangeUtils.setAlreadyRouted(exchange); exchange.getResponse().getHeaders().add("TestHeader", "value1"); exchange.getResponse().getHeaders().add("TestHeader", "value2"); return chain.filter(exchange).then(Mono.defer(() -> { exchange.getResponse().getHeaders().set(CONTENT_TYPE, "application/json"); DataBuffer buffer = exchange.getResponse().bufferFactory().wrap("{\"test\":\"YYY\"}".getBytes()); return exchange.getResponse().writeWith(Flux.just(buffer)); })); } @Override public int getOrder () { return 2; } }
And the stacktrace:
2021-03-26 13:21:54.144 ERROR 17224 --- [ctor-http-nio-2] o.s.w.s.adapter.HttpWebHandlerAdapter : [17552e31-1] Error [java.lang.UnsupportedOperationException] for HTTP GET "/service/httpbin", but ServerHttpResponse already committed (200 OK) 2021-03-26 13:21:54.148 ERROR 17224 --- [ctor-http-nio-2] r.n.http.server.HttpServerOperations : [id:17552e31-1, L:/0:0:0:0:0:0:0:1:8080 - R:/0:0:0:0:0:0:0:1:63058] Error finishing response. Closing connection java.lang.UnsupportedOperationException: null at org.springframework.http.ReadOnlyHttpHeaders.set(ReadOnlyHttpHeaders.java:106) ~[spring-web-5.3.5.jar:5.3.5] Suppressed: reactor.core.publisher.FluxOnAssembly$OnAssemblyException: Error has been observed at the following site(s): |_ checkpoint ⇢ org.springframework.cloud.gateway.filter.WeightCalculatorWebFilter [DefaultWebFilterChain] |_ checkpoint ⇢ HTTP GET "/service/httpbin" [ExceptionHandlingWebHandler] Stack trace: at org.springframework.http.ReadOnlyHttpHeaders.set(ReadOnlyHttpHeaders.java:106) ~[spring-web-5.3.5.jar:5.3.5] at org.springframework.cloud.gateway.filter.factory.DedupeResponseHeaderGatewayFilterFactory.dedupe(DedupeResponseHeaderGatewayFilterFactory.java:143) ~[spring-cloud-gateway-server-3.0.2.jar:3.0.2] at org.springframework.cloud.gateway.filter.factory.DedupeResponseHeaderGatewayFilterFactory.dedupe(DedupeResponseHeaderGatewayFilterFactory.java:129) ~[spring-cloud-gateway-server-3.0.2.jar:3.0.2] at org.springframework.cloud.gateway.filter.factory.DedupeResponseHeaderGatewayFilterFactory$1.lambda$filter$0(DedupeResponseHeaderGatewayFilterFactory.java:92) ~[spring-cloud-gateway-server-3.0.2.jar:3.0.2] at reactor.core.publisher.MonoRunnable.call(MonoRunnable.java:73) ~[reactor-core-3.4.4.jar:3.4.4] at reactor.core.publisher.MonoRunnable.call(MonoRunnable.java:32) ~[reactor-core-3.4.4.jar:3.4.4] at reactor.core.publisher.MonoIgnoreThen$ThenIgnoreMain.drain(MonoIgnoreThen.java:136) ~[reactor-core-3.4.4.jar:3.4.4] at reactor.core.publisher.MonoIgnoreThen$ThenIgnoreMain.ignoreDone(MonoIgnoreThen.java:191) ~[reactor-core-3.4.4.jar:3.4.4] at reactor.core.publisher.MonoIgnoreThen$ThenIgnoreInner.onComplete(MonoIgnoreThen.java:248) ~[reactor-core-3.4.4.jar:3.4.4] at reactor.core.publisher.Operators$MonoSubscriber.onComplete(Operators.java:1857) ~[reactor-core-3.4.4.jar:3.4.4] at reactor.core.publisher.MonoIgnoreThen$ThenAcceptInner.onComplete(MonoIgnoreThen.java:323) ~[reactor-core-3.4.4.jar:3.4.4] at reactor.core.publisher.MonoPeekTerminal$MonoTerminalPeekSubscriber.onComplete(MonoPeekTerminal.java:299) ~[reactor-core-3.4.4.jar:3.4.4] at org.springframework.http.server.reactive.ChannelSendOperator$WriteCompletionBarrier.onComplete(ChannelSendOperator.java:423) ~[spring-web-5.3.5.jar:5.3.5] at reactor.core.publisher.MonoIgnoreElements$IgnoreElementsSubscriber.onComplete(MonoIgnoreElements.java:88) ~[reactor-core-3.4.4.jar:3.4.4] at reactor.core.publisher.FluxConcatArray$ConcatArraySubscriber.onComplete(FluxConcatArray.java:192) ~[reactor-core-3.4.4.jar:3.4.4] at reactor.core.publisher.Operators$MonoSubscriber.onComplete(Operators.java:1857) ~[reactor-core-3.4.4.jar:3.4.4] at reactor.core.publisher.MonoIgnoreThen$ThenAcceptInner.onComplete(MonoIgnoreThen.java:323) ~[reactor-core-3.4.4.jar:3.4.4] at reactor.netty.channel.MonoSendMany$SendManyInner.run(MonoSendMany.java:349) ~[reactor-netty-core-1.0.5.jar:1.0.5] at reactor.netty.channel.MonoSendMany$SendManyInner.trySchedule(MonoSendMany.java:423) ~[reactor-netty-core-1.0.5.jar:1.0.5] at reactor.netty.channel.MonoSendMany$SendManyInner.trySuccess(MonoSendMany.java:582) ~[reactor-netty-core-1.0.5.jar:1.0.5] at reactor.netty.channel.MonoSendMany$SendManyInner.trySuccess(MonoSendMany.java:118) ~[reactor-netty-core-1.0.5.jar:1.0.5] at io.netty.util.concurrent.PromiseCombiner.tryPromise(PromiseCombiner.java:170) ~[netty-common-4.1.60.Final.jar:4.1.60.Final] at io.netty.util.concurrent.PromiseCombiner.access$600(PromiseCombiner.java:35) ~[netty-common-4.1.60.Final.jar:4.1.60.Final] at io.netty.util.concurrent.PromiseCombiner$1.operationComplete0(PromiseCombiner.java:62) ~[netty-common-4.1.60.Final.jar:4.1.60.Final] at io.netty.util.concurrent.PromiseCombiner$1.operationComplete(PromiseCombiner.java:44) ~[netty-common-4.1.60.Final.jar:4.1.60.Final] at io.netty.util.concurrent.DefaultPromise.notifyListener0(DefaultPromise.java:578) ~[netty-common-4.1.60.Final.jar:4.1.60.Final] at io.netty.util.concurrent.DefaultPromise.notifyListenersNow(DefaultPromise.java:552) ~[netty-common-4.1.60.Final.jar:4.1.60.Final] at io.netty.util.concurrent.DefaultPromise.notifyListeners(DefaultPromise.java:491) ~[netty-common-4.1.60.Final.jar:4.1.60.Final] at io.netty.util.concurrent.DefaultPromise.setValue0(DefaultPromise.java:616) ~[netty-common-4.1.60.Final.jar:4.1.60.Final] at io.netty.util.concurrent.DefaultPromise.setSuccess0(DefaultPromise.java:605) ~[netty-common-4.1.60.Final.jar:4.1.60.Final] at io.netty.util.concurrent.DefaultPromise.trySuccess(DefaultPromise.java:104) ~[netty-common-4.1.60.Final.jar:4.1.60.Final] at io.netty.util.internal.PromiseNotificationUtil.trySuccess(PromiseNotificationUtil.java:48) ~[netty-common-4.1.60.Final.jar:4.1.60.Final] at io.netty.channel.ChannelOutboundBuffer.safeSuccess(ChannelOutboundBuffer.java:717) ~[netty-transport-4.1.60.Final.jar:4.1.60.Final] at io.netty.channel.ChannelOutboundBuffer.remove(ChannelOutboundBuffer.java:272) ~[netty-transport-4.1.60.Final.jar:4.1.60.Final] at io.netty.channel.ChannelOutboundBuffer.removeBytes(ChannelOutboundBuffer.java:352) ~[netty-transport-4.1.60.Final.jar:4.1.60.Final] at io.netty.channel.socket.nio.NioSocketChannel.doWrite(NioSocketChannel.java:431) ~[netty-transport-4.1.60.Final.jar:4.1.60.Final] at io.netty.channel.AbstractChannel$AbstractUnsafe.flush0(AbstractChannel.java:941) ~[netty-transport-4.1.60.Final.jar:4.1.60.Final] at io.netty.channel.nio.AbstractNioChannel$AbstractNioUnsafe.flush0(AbstractNioChannel.java:354) ~[netty-transport-4.1.60.Final.jar:4.1.60.Final] at io.netty.channel.AbstractChannel$AbstractUnsafe.flush(AbstractChannel.java:905) ~[netty-transport-4.1.60.Final.jar:4.1.60.Final] at io.netty.channel.DefaultChannelPipeline$HeadContext.flush(DefaultChannelPipeline.java:1372) ~[netty-transport-4.1.60.Final.jar:4.1.60.Final] at io.netty.channel.AbstractChannelHandlerContext.invokeFlush0(AbstractChannelHandlerContext.java:750) ~[netty-transport-4.1.60.Final.jar:4.1.60.Final] at io.netty.channel.AbstractChannelHandlerContext.invokeFlush(AbstractChannelHandlerContext.java:742) ~[netty-transport-4.1.60.Final.jar:4.1.60.Final] at io.netty.channel.AbstractChannelHandlerContext.flush(AbstractChannelHandlerContext.java:728) ~[netty-transport-4.1.60.Final.jar:4.1.60.Final] at io.netty.channel.CombinedChannelDuplexHandler$DelegatingChannelHandlerContext.flush(CombinedChannelDuplexHandler.java:531) ~[netty-transport-4.1.60.Final.jar:4.1.60.Final] at io.netty.channel.ChannelOutboundHandlerAdapter.flush(ChannelOutboundHandlerAdapter.java:125) ~[netty-transport-4.1.60.Final.jar:4.1.60.Final] at io.netty.channel.CombinedChannelDuplexHandler.flush(CombinedChannelDuplexHandler.java:356) ~[netty-transport-4.1.60.Final.jar:4.1.60.Final] at io.netty.channel.AbstractChannelHandlerContext.invokeFlush0(AbstractChannelHandlerContext.java:750) ~[netty-transport-4.1.60.Final.jar:4.1.60.Final] at io.netty.channel.AbstractChannelHandlerContext.invokeFlush(AbstractChannelHandlerContext.java:742) ~[netty-transport-4.1.60.Final.jar:4.1.60.Final] at io.netty.channel.AbstractChannelHandlerContext.flush(AbstractChannelHandlerContext.java:728) ~[netty-transport-4.1.60.Final.jar:4.1.60.Final] at reactor.netty.channel.MonoSendMany$SendManyInner$AsyncFlush.run(MonoSendMany.java:778) ~[reactor-netty-core-1.0.5.jar:1.0.5] at io.netty.util.concurrent.AbstractEventExecutor.safeExecute$$$capture(AbstractEventExecutor.java:164) ~[netty-common-4.1.60.Final.jar:4.1.60.Final] at io.netty.util.concurrent.AbstractEventExecutor.safeExecute(AbstractEventExecutor.java) ~[netty-common-4.1.60.Final.jar:4.1.60.Final] at io.netty.util.concurrent.SingleThreadEventExecutor.runAllTasks(SingleThreadEventExecutor.java:472) ~[netty-common-4.1.60.Final.jar:4.1.60.Final] at io.netty.channel.nio.NioEventLoop.run(NioEventLoop.java:497) ~[netty-transport-4.1.60.Final.jar:4.1.60.Final] at io.netty.util.concurrent.SingleThreadEventExecutor$4.run(SingleThreadEventExecutor.java:989) ~[netty-common-4.1.60.Final.jar:4.1.60.Final] at io.netty.util.internal.ThreadExecutorMap$2.run(ThreadExecutorMap.java:74) ~[netty-common-4.1.60.Final.jar:4.1.60.Final] at io.netty.util.concurrent.FastThreadLocalRunnable.run(FastThreadLocalRunnable.java:30) ~[netty-common-4.1.60.Final.jar:4.1.60.Final] at java.base/java.lang.Thread.run(Thread.java:834) ~[na:na]
I have same problem like this, are you resolve?
Hi @fengzheng0571 , I have stopped using the dedupe filter and created another filter that would do what I need (dedupe headers)
@edoina Thanks for reply, I get it.
Describe the bug When the CircuitBreaker cut itself, it seems some answer is committed before the DedupHeader
Stack
Sample I've not been able to craft a reproducing case yet