spring-cloud / spring-cloud/spring-cloud-gateway

UnsupportedOperationException in NettyRoutingFilter when handling immutable ReadOnlyHttpHeaders

Open
#4,270 1 comment 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

waiting-for-triage
Dominant language
Java
Stars
4.9k
Forks
3.5k
Avg merge
20h 57m
Merged PRs (30d)
8

Description

Describe the bug
When routing requests through Spring Cloud Gateway, the NettyRoutingFilter attempts to remove headers from a ReadOnlyHttpHeaders instance. This results in a java.lang.UnsupportedOperationException.

Although the response is still committed successfully (200 OK), the error is logged, which is misleading and suggests unexpected behavior in the filter chain.

Spring Boot version: 4.0.6/4.1.1
Spring Cloud Gateway: 2025.1.1/20.25.1.2

Sample
No sample application but i can try to create if need.
Logs:

2026-08-26T08:37:40.345Z TRACE 1 --- [gateway] [or-http-epoll-3] o.s.w.s.adapter.HttpWebHandlerAdapter    : [37da73a0-12] HTTP GET "/AAA/BBB", headers={masked}
2026-08-26T08:37:40.421Z  INFO 1 --- [gateway] [or-http-epoll-3] c.c.a.hub.gateway.config.LoggingFilter   : Incoming request https://example.com/AAA/BBB is routed to id: BBB-ui, uri: http://BBB-ui:80/AAA/BBB
2026-08-26T08:37:40.445Z ERROR 1 --- [gateway] [or-http-epoll-3] c.c.a.h.g.h.TopLevelExceptionHandler     : Caught exception

java.lang.UnsupportedOperationException
        at org.springframework.http.ReadOnlyHttpHeaders.remove(ReadOnlyHttpHeaders.java:166) ~[spring-web-7.0.9.jar!/:7.0.9]
        Suppressed: The stacktrace has been enhanced by Reactor, refer to additional information below: 
Error has been observed at the following site(s):
        *__checkpoint ⇢ org.springframework.cloud.gateway.filter.WeightCalculatorWebFilter@30704f85
        *__checkpoint ⇢ org.springframework.security.web.server.authorization.AuthorizationWebFilter@43115b17
        *__checkpoint ⇢ org.springframework.security.web.server.authorization.ExceptionTranslationWebFilter@79976e07
        *__checkpoint ⇢ org.springframework.security.web.server.authentication.logout.LogoutWebFilter@4f229e87
        *__checkpoint ⇢ org.springframework.security.web.server.savedrequest.ServerRequestCacheWebFilter@2e66a2b
        *__checkpoint ⇢ org.springframework.security.web.server.context.SecurityContextServerWebExchangeWebFilter@2475bbb3
        *__checkpoint ⇢ org.springframework.security.web.server.authentication.AuthenticationWebFilter@12284f0d
        *__checkpoint ⇢ org.springframework.security.oauth2.client.web.server.authentication.OAuth2LoginAuthenticationWebFilter@21cf894
        *__checkpoint ⇢ org.springframework.security.oauth2.client.web.server.OAuth2AuthorizationRequestRedirectWebFilter@28fbda1c
        *__checkpoint ⇢ org.springframework.security.web.server.context.ReactorContextWebFilter@682d6318
        *__checkpoint ⇢ org.springframework.security.web.server.header.HttpHeaderWriterWebFilter@5515313b
        *__checkpoint ⇢ org.springframework.security.config.web.server.ServerHttpSecurity$ServerWebExchangeReactorContextWebFilter@2c02c49e
        *__checkpoint ⇢ org.springframework.security.web.server.WebFilterChainProxy@32328dc4
        *__checkpoint ⇢ HTTP GET "/AAA/BBB" [ExceptionHandlingWebHandler]
Original Stack Trace:
                at org.springframework.http.ReadOnlyHttpHeaders.remove(ReadOnlyHttpHeaders.java:166) ~[spring-web-7.0.9.jar!/:7.0.9]
                at org.springframework.cloud.gateway.filter.NettyRoutingFilter.lambda$filter$4(NettyRoutingFilter.java:182) ~[spring-cloud-gateway-server-webflux-5.0.2.jar!/:5.0.2]
                at reactor.netty.http.client.HttpClientFinalizer.lambda$responseConnection$4(HttpClientFinalizer.java:102) ~[reactor-netty-http-1.3.7.jar!/:1.3.7]
                at reactor.core.publisher.MonoFlatMapMany$FlatMapManyMain.onNext(MonoFlatMapMany.java:164) ~[reactor-core-3.8.7.jar!/:3.8.7]
                at reactor.core.publisher.SerializedSubscriber.onNext(SerializedSubscriber.java:100) ~[reactor-core-3.8.7.jar!/:3.8.7]
                at reactor.core.publisher.FluxRetryWhen$RetryWhenMainSubscriber.onNext(FluxRetryWhen.java:179) ~[reactor-core-3.8.7.jar!/:3.8.7]
                at reactor.core.publisher.MonoCreate$DefaultMonoSink.success(MonoCreate.java:177) ~[reactor-core-3.8.7.jar!/:3.8.7]
                at reactor.netty.http.client.HttpClientConnect$HttpIOHandlerObserver.onStateChange(HttpClientConnect.java:460) ~[reactor-netty-http-1.3.7.jar!/:1.3.7]
                at reactor.netty.ReactorNetty$CompositeConnectionObserver.onStateChange(ReactorNetty.java:736) ~[reactor-netty-core-1.3.7.jar!/:1.3.7]
                at reactor.netty.resources.DefaultPooledConnectionProvider$DisposableAcquire.onStateChange(DefaultPooledConnectionProvider.java:229) ~[reactor-netty-core-1.3.7.jar!/:1.3.7]
                at reactor.netty.resources.DefaultPooledConnectionProvider$PooledConnection.onStateChange(DefaultPooledConnectionProvider.java:491) ~[reactor-netty-core-1.3.7.jar!/:1.3.7]
                at reactor.netty.http.client.HttpClientOperations.onInboundNext(HttpClientOperations.java:858) ~[reactor-netty-http-1.3.7.jar!/:1.3.7]
                at reactor.netty.channel.ChannelOperationsHandler.channelRead(ChannelOperationsHandler.java:115) ~[reactor-netty-core-1.3.7.jar!/:1.3.7]
                at io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:357) ~[netty-transport-4.2.17.Final.jar!/:4.2.17.Final]
                at io.netty.channel.CombinedChannelDuplexHandler$DelegatingChannelHandlerContext.fireChannelRead(CombinedChannelDuplexHandler.java:434) ~[netty-transport-4.2.17.Final.jar!/:4.2.17.Final]
                at io.netty.handler.codec.ByteToMessageDecoder.fireChannelRead(ByteToMessageDecoder.java:361) ~[netty-codec-base-4.2.17.Final.jar!/:4.2.17.Final]
                at io.netty.handler.codec.ByteToMessageDecoder.fireChannelRead(ByteToMessageDecoder.java:348) ~[netty-codec-base-4.2.17.Final.jar!/:4.2.17.Final]
                at io.netty.handler.codec.ByteToMessageDecoder.callDecode(ByteToMessageDecoder.java:470) ~[netty-codec-base-4.2.17.Final.jar!/:4.2.17.Final]
                at io.netty.handler.codec.ByteToMessageDecoder.channelRead(ByteToMessageDecoder.java:296) ~[netty-codec-base-4.2.17.Final.jar!/:4.2.17.Final]
                at io.netty.channel.CombinedChannelDuplexHandler.channelRead(CombinedChannelDuplexHandler.java:249) ~[netty-transport-4.2.17.Final.jar!/:4.2.17.Final]
                at io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:355) ~[netty-transport-4.2.17.Final.jar!/:4.2.17.Final]
                at io.netty.channel.DefaultChannelPipeline$HeadContext.channelRead(DefaultChannelPipeline.java:1429) ~[netty-transport-4.2.17.Final.jar!/:4.2.17.Final]
                at io.netty.channel.DefaultChannelPipeline.fireChannelRead(DefaultChannelPipeline.java:918) ~[netty-transport-4.2.17.Final.jar!/:4.2.17.Final]
                at io.netty.channel.epoll.AbstractEpollStreamChannel$EpollStreamUnsafe.epollInReady(AbstractEpollStreamChannel.java:804) ~[netty-transport-classes-epoll-4.2.17.Final.jar!/:4.2.17.Final]
                at io.netty.channel.epoll.AbstractEpollChannel$AbstractEpollUnsafe.handle(AbstractEpollChannel.java:487) ~[netty-transport-classes-epoll-4.2.17.Final.jar!/:4.2.17.Final]
                at io.netty.channel.epoll.EpollIoHandler$DefaultEpollIoRegistration.handle(EpollIoHandler.java:349) ~[netty-transport-classes-epoll-4.2.17.Final.jar!/:4.2.17.Final]
                at io.netty.channel.epoll.EpollIoHandler.processReady(EpollIoHandler.java:548) ~[netty-transport-classes-epoll-4.2.17.Final.jar!/:4.2.17.Final]
                at io.netty.channel.epoll.EpollIoHandler.run(EpollIoHandler.java:491) ~[netty-transport-classes-epoll-4.2.17.Final.jar!/:4.2.17.Final]
                at io.netty.channel.SingleThreadIoEventLoop.runIo(SingleThreadIoEventLoop.java:225) ~[netty-transport-4.2.17.Final.jar!/:4.2.17.Final]
                at io.netty.channel.SingleThreadIoEventLoop.run(SingleThreadIoEventLoop.java:196) ~[netty-transport-4.2.17.Final.jar!/:4.2.17.Final]
                at io.netty.util.concurrent.SingleThreadEventExecutor$5.run(SingleThreadEventExecutor.java:1204) ~[netty-common-4.2.17.Final.jar!/:4.2.17.Final]
                at io.netty.util.internal.ThreadExecutorMap$2.run(ThreadExecutorMap.java:74) ~[netty-common-4.2.17.Final.jar!/:4.2.17.Final]
                at io.netty.util.concurrent.FastThreadLocalRunnable.run(FastThreadLocalRunnable.java:30) ~[netty-common-4.2.17.Final.jar!/:4.2.17.Final]
                at java.base/java.lang.Thread.run(Unknown Source) ~[na:na]

2026-08-26T08:37:40.466Z ERROR 1 --- [gateway] [or-http-epoll-3] o.s.w.s.adapter.HttpWebHandlerAdapter    : [37da73a0-12] Error [java.lang.UnsupportedOperationException] for HTTP GET "/AAA/BBB", but ServerHttpResponse already committed (200 OK)

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start at NettyRoutingFilter.lambda$filter$4 around line 182, where the stack trace shows removal from ReadOnlyHttpHeaders, and compare this with the request routing flow described in the issue. Reproduce the request if possible, then verify that routing still returns 200 OK without logging an UnsupportedOperationException or a misleading committed-response error.

Written by the indexing model from the issue text.

Assessment

Tech stack
java, spring, spring-boot
Domain
api, backend
Issue type
Bug
Difficulty
3/5
Estimated time
1-2 days
Activity status
Active
Clarity
Mostly clear
Newbie friendliness
55/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.