spring-cloud / spring-cloud-gateway

An API Gateway built on Spring Framework and Spring Boot providing routing and more.
http://cloud.spring.io
Apache License 2.0
4.5k stars 3.31k forks source link

Issue with DedupHeaders when CircuitBreaker activates #1822

Open blacelle opened 4 years ago

blacelle commented 4 years ago

Describe the bug When the CircuitBreaker cut itself, it seems some answer is committed before the DedupHeader

Stack

2020-07-08 12:16:13,035 DEBUG[reactor-http-nio-3] o.s.w.r.r.m.a.RequestMappingHandlerMapping|lambda$getHandler$1(183) - [7d9887a6-1] Mapped to io.myproject.GatewayController#someMethod()
2020-07-08 12:16:13,087 ERROR[reactor-http-nio-3] o.s.w.s.a.HttpWebHandlerAdapter|handleUnresolvedError(295) - [7d9887a6-1] Error [java.lang.UnsupportedOperationException] for HTTP GET "/my-app/route", but ServerHttpResponse already committed (302 FOUND)
2020-07-08 12:16:13,090 ERROR[reactor-http-nio-3] r.n.http.server.HttpServerOperations|error(319) - [id: 0x7d9887a6, L:/127.0.0.1:65264 - R:/127.0.0.1:65265] Error starting response. Replying error status
java.lang.UnsupportedOperationException: null
    at org.springframework.http.ReadOnlyHttpHeaders.set(ReadOnlyHttpHeaders.java:106)
    Suppressed: reactor.core.publisher.FluxOnAssembly$OnAssemblyException: 
Error has been observed at the following site(s):
    |_ checkpoint ⇢ org.springframework.cloud.gateway.filter.WeightCalculatorWebFilter [DefaultWebFilterChain]
    |_ checkpoint ⇢ org.springframework.boot.actuate.metrics.web.reactive.server.MetricsWebFilter [DefaultWebFilterChain]
    |_ checkpoint ⇢ HTTP GET "/my-app/route" [ExceptionHandlingWebHandler]
Stack trace:
        at org.springframework.http.ReadOnlyHttpHeaders.set(ReadOnlyHttpHeaders.java:106)
        at org.springframework.cloud.gateway.filter.factory.DedupeResponseHeaderGatewayFilterFactory.dedupe(DedupeResponseHeaderGatewayFilterFactory.java:146)
        at org.springframework.cloud.gateway.filter.factory.DedupeResponseHeaderGatewayFilterFactory.dedupe(DedupeResponseHeaderGatewayFilterFactory.java:132)
        at org.springframework.cloud.gateway.filter.factory.DedupeResponseHeaderGatewayFilterFactory$1.lambda$filter$0(DedupeResponseHeaderGatewayFilterFactory.java:93)
        at reactor.core.publisher.MonoRunnable.call(MonoRunnable.java:73)
        at reactor.core.publisher.MonoRunnable.call(MonoRunnable.java:32)
        at reactor.core.publisher.MonoIgnoreThen$ThenIgnoreMain.drain(MonoIgnoreThen.java:135)
        at reactor.core.publisher.MonoIgnoreThen$ThenIgnoreMain.ignoreDone(MonoIgnoreThen.java:190)
        at reactor.core.publisher.MonoIgnoreThen$ThenIgnoreInner.onComplete(MonoIgnoreThen.java:240)
        at reactor.core.publisher.Operators$MultiSubscriptionSubscriber.onComplete(Operators.java:2016)
        at reactor.core.publisher.Operators$MultiSubscriptionSubscriber.onComplete(Operators.java:2016)
        at reactor.core.publisher.MonoFlatMap$FlatMapMain.secondComplete(MonoFlatMap.java:189)
        at reactor.core.publisher.MonoFlatMap$FlatMapInner.onComplete(MonoFlatMap.java:260)
        at reactor.core.publisher.Operators$MultiSubscriptionSubscriber.onComplete(Operators.java:2016)
        at reactor.core.publisher.FluxHide$SuppressFuseableSubscriber.onComplete(FluxHide.java:137)
        at reactor.core.publisher.MonoIgnoreElements$IgnoreElementsSubscriber.onComplete(MonoIgnoreElements.java:81)
        at reactor.core.publisher.FluxConcatArray$ConcatArraySubscriber.onComplete(FluxConcatArray.java:191)
        at reactor.core.publisher.FluxConcatArray.subscribe(FluxConcatArray.java:80)
        at reactor.core.publisher.Mono.subscribe(Mono.java:4219)
        at reactor.core.publisher.FluxFlatMap.trySubscribeScalarMap(FluxFlatMap.java:202)
        at reactor.core.publisher.MonoFlatMap.subscribeOrReturn(MonoFlatMap.java:53)
        at reactor.core.publisher.InternalMonoOperator.subscribe(InternalMonoOperator.java:57)
        at reactor.core.publisher.MonoFlatMap$FlatMapMain.onNext(MonoFlatMap.java:150)
        at reactor.core.publisher.Operators$MonoSubscriber.complete(Operators.java:1782)
        at reactor.core.publisher.MonoFlatMap$FlatMapInner.onNext(MonoFlatMap.java:241)
        at reactor.core.publisher.FluxOnErrorResume$ResumeSubscriber.onNext(FluxOnErrorResume.java:73)
        at reactor.core.publisher.FluxPeekFuseable$PeekFuseableSubscriber.onNext(FluxPeekFuseable.java:203)
        at reactor.core.publisher.FluxPeekFuseable$PeekFuseableSubscriber.onNext(FluxPeekFuseable.java:203)
        at reactor.core.publisher.Operators$MonoSubscriber.complete(Operators.java:1782)
        at reactor.core.publisher.MonoIgnoreThen$ThenAcceptInner.onNext(MonoIgnoreThen.java:296)
        at reactor.core.publisher.Operators$ScalarSubscription.request(Operators.java:2344)
        at reactor.core.publisher.MonoIgnoreThen$ThenAcceptInner.onSubscribe(MonoIgnoreThen.java:285)
        at reactor.core.publisher.FluxFlatMap.trySubscribeScalarMap(FluxFlatMap.java:191)
        at reactor.core.publisher.MonoFlatMap.subscribeOrReturn(MonoFlatMap.java:53)
        at reactor.core.publisher.InternalMonoOperator.subscribe(InternalMonoOperator.java:57)
        at reactor.core.publisher.MonoDefer.subscribe(MonoDefer.java:52)
        at reactor.core.publisher.MonoIgnoreThen$ThenIgnoreMain.drain(MonoIgnoreThen.java:153)
        at reactor.core.publisher.MonoIgnoreThen.subscribe(MonoIgnoreThen.java:56)
        at reactor.core.publisher.InternalMonoOperator.subscribe(InternalMonoOperator.java:64)
        at reactor.core.publisher.MonoFlatMap$FlatMapMain.onNext(MonoFlatMap.java:150)
        at reactor.core.publisher.FluxSwitchIfEmpty$SwitchIfEmptySubscriber.onNext(FluxSwitchIfEmpty.java:67)
        at reactor.core.publisher.MonoNext$NextSubscriber.onNext(MonoNext.java:76)
        at reactor.core.publisher.FluxConcatMap$ConcatMapImmediate.innerNext(FluxConcatMap.java:274)
        at reactor.core.publisher.FluxConcatMap$ConcatMapInner.onNext(FluxConcatMap.java:851)
        at reactor.core.publisher.FluxMapFuseable$MapFuseableSubscriber.onNext(FluxMapFuseable.java:121)
        at reactor.core.publisher.MonoPeekTerminal$MonoTerminalPeekSubscriber.onNext(MonoPeekTerminal.java:173)
        at reactor.core.publisher.Operators$ScalarSubscription.request(Operators.java:2344)
        at reactor.core.publisher.MonoPeekTerminal$MonoTerminalPeekSubscriber.request(MonoPeekTerminal.java:132)
        at reactor.core.publisher.FluxMapFuseable$MapFuseableSubscriber.request(FluxMapFuseable.java:162)
        at reactor.core.publisher.Operators$MultiSubscriptionSubscriber.set(Operators.java:2152)
        at reactor.core.publisher.Operators$MultiSubscriptionSubscriber.onSubscribe(Operators.java:2026)
        at reactor.core.publisher.FluxMapFuseable$MapFuseableSubscriber.onSubscribe(FluxMapFuseable.java:90)
        at reactor.core.publisher.MonoPeekTerminal$MonoTerminalPeekSubscriber.onSubscribe(MonoPeekTerminal.java:145)
        at reactor.core.publisher.MonoJust.subscribe(MonoJust.java:54)
        at reactor.core.publisher.Mono.subscribe(Mono.java:4219)
        at reactor.core.publisher.FluxConcatMap$ConcatMapImmediate.drain(FluxConcatMap.java:441)
        at reactor.core.publisher.FluxConcatMap$ConcatMapImmediate.onSubscribe(FluxConcatMap.java:211)
        at reactor.core.publisher.FluxIterable.subscribe(FluxIterable.java:161)
        at reactor.core.publisher.FluxIterable.subscribe(FluxIterable.java:86)
        at reactor.core.publisher.Mono.subscribe(Mono.java:4219)
        at reactor.core.publisher.FluxOnErrorResume$ResumeSubscriber.onError(FluxOnErrorResume.java:97)
        at reactor.core.publisher.MonoPeekTerminal$MonoTerminalPeekSubscriber.onError(MonoPeekTerminal.java:251)
        at reactor.core.publisher.SerializedSubscriber.onError(SerializedSubscriber.java:124)
        at reactor.core.publisher.FluxTimeout$TimeoutMainSubscriber.onError(FluxTimeout.java:213)
        at io.github.resilience4j.reactor.circuitbreaker.operator.CircuitBreakerSubscriber.hookOnError(CircuitBreakerSubscriber.java:87)
        at reactor.core.publisher.BaseSubscriber.onError(BaseSubscriber.java:180)
        at reactor.core.publisher.Operators$MonoSubscriber.onError(Operators.java:1829)
        at reactor.core.publisher.MonoIgnoreThen$ThenIgnoreInner.onError(MonoIgnoreThen.java:235)
        at reactor.core.publisher.Operators$MonoSubscriber.onError(Operators.java:1829)
        at reactor.core.publisher.MonoIgnoreThen$ThenAcceptInner.onError(MonoIgnoreThen.java:306)
        at reactor.core.publisher.Operators.error(Operators.java:196)
        at reactor.core.publisher.MonoError.subscribe(MonoError.java:52)
        at reactor.core.publisher.MonoDefer.subscribe(MonoDefer.java:52)
        at reactor.core.publisher.MonoIgnoreThen$ThenIgnoreMain.drain(MonoIgnoreThen.java:153)
        at reactor.core.publisher.MonoIgnoreThen$ThenIgnoreMain.ignoreDone(MonoIgnoreThen.java:190)
        at reactor.core.publisher.MonoIgnoreThen$ThenIgnoreInner.onComplete(MonoIgnoreThen.java:240)
        at reactor.core.publisher.Operators$MonoSubscriber.onComplete(Operators.java:1824)
        at reactor.core.publisher.MonoIgnoreThen$ThenAcceptInner.onComplete(MonoIgnoreThen.java:314)
        at reactor.core.publisher.Operators.complete(Operators.java:135)
        at reactor.core.publisher.MonoEmpty.subscribe(MonoEmpty.java:45)
        at reactor.core.publisher.MonoDefer.subscribe(MonoDefer.java:52)
        at reactor.core.publisher.MonoDefer.subscribe(MonoDefer.java:52)
        at reactor.core.publisher.MonoIgnoreThen$ThenIgnoreMain.drain(MonoIgnoreThen.java:153)
        at reactor.core.publisher.MonoIgnoreThen$ThenIgnoreMain.ignoreDone(MonoIgnoreThen.java:190)
        at reactor.core.publisher.MonoIgnoreThen$ThenIgnoreInner.onComplete(MonoIgnoreThen.java:240)
        at reactor.core.publisher.Operators$ScalarSubscription.request(Operators.java:2346)
        at reactor.core.publisher.MonoFlatMapMany$FlatMapManyMain.onSubscribeInner(MonoFlatMapMany.java:143)
        at reactor.core.publisher.MonoFlatMapMany$FlatMapManyMain.onNext(MonoFlatMapMany.java:182)
        at reactor.core.publisher.SerializedSubscriber.onNext(SerializedSubscriber.java:99)
        at reactor.core.publisher.FluxRetryWhen$RetryWhenMainSubscriber.onNext(FluxRetryWhen.java:162)
        at reactor.core.publisher.MonoCreate$DefaultMonoSink.success(MonoCreate.java:156)
        at reactor.netty.http.client.HttpClientConnect$HttpIOHandlerObserver.onStateChange(HttpClientConnect.java:431)
        at reactor.netty.ReactorNetty$CompositeConnectionObserver.onStateChange(ReactorNetty.java:514)
        at reactor.netty.resources.PooledConnectionProvider$DisposableAcquire.onStateChange(PooledConnectionProvider.java:536)
        at reactor.netty.resources.PooledConnectionProvider$PooledConnection.onStateChange(PooledConnectionProvider.java:427)
        at reactor.netty.http.client.HttpClientOperations.onInboundNext(HttpClientOperations.java:562)
        at reactor.netty.channel.ChannelOperationsHandler.channelRead(ChannelOperationsHandler.java:96)
        at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:379)
        at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:365)
        at io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:357)
        at io.netty.channel.CombinedChannelDuplexHandler$DelegatingChannelHandlerContext.fireChannelRead(CombinedChannelDuplexHandler.java:436)
        at io.netty.handler.codec.ByteToMessageDecoder.fireChannelRead(ByteToMessageDecoder.java:324)
        at io.netty.handler.codec.ByteToMessageDecoder.channelRead(ByteToMessageDecoder.java:296)
        at io.netty.channel.CombinedChannelDuplexHandler.channelRead(CombinedChannelDuplexHandler.java:251)
        at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:379)
        at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:365)
        at io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:357)
        at io.netty.channel.DefaultChannelPipeline$HeadContext.channelRead(DefaultChannelPipeline.java:1410)
        at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:379)
        at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:365)
        at io.netty.channel.DefaultChannelPipeline.fireChannelRead(DefaultChannelPipeline.java:919)
        at io.netty.channel.nio.AbstractNioByteChannel$NioByteUnsafe.read(AbstractNioByteChannel.java:163)
        at io.netty.channel.nio.NioEventLoop.processSelectedKey(NioEventLoop.java:714)
        at io.netty.channel.nio.NioEventLoop.processSelectedKeysOptimized(NioEventLoop.java:650)
        at io.netty.channel.nio.NioEventLoop.processSelectedKeys(NioEventLoop.java:576)
        at io.netty.channel.nio.NioEventLoop.run(NioEventLoop.java:493)
        at io.netty.util.concurrent.SingleThreadEventExecutor$4.run(SingleThreadEventExecutor.java:989)
        at io.netty.util.internal.ThreadExecutorMap$2.run(ThreadExecutorMap.java:74)
        at io.netty.util.concurrent.FastThreadLocalRunnable.run(FastThreadLocalRunnable.java:30)
        at java.base/java.lang.Thread.run(Thread.java:834)

Sample I've not been able to craft a reproducing case yet

spencergibb commented 4 years ago

let us know when you have something that reproduces it

blacelle commented 4 years ago

@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

Screenshot 2020-07-08 at 20 57 02
    <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>
eugeniusd commented 3 years ago

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]
fengzheng0571 commented 3 years ago

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?

edoina commented 3 years ago

Hi @fengzheng0571 , I have stopped using the dedupe filter and created another filter that would do what I need (dedupe headers)

fengzheng0571 commented 3 years ago

@edoina Thanks for reply, I get it.