Spring-cloud-gateway: Question. How can i modify/add response headers from GlobalFilter.

Created on 3 Jan 2019  Â·  14Comments  Â·  Source: spring-cloud/spring-cloud-gateway

Hi,
Could you please help me for how to modify the response headers from Global Filter.
When i use, serverWebExchange.getResponse().getHeaders().add(headerName, headerValue)
it is throwing UnsupportedOperationException from UnsupportedOperationException.
Thanks in advance for your help

question

Most helpful comment

Try this

         ServerHttpResponse response = exchange.getResponse();
         response.beforeCommit(() -> {
                     response.getHeaders().set(key,value));
             }
             return Mono.empty();
         });

All 14 comments

exception stack trace while modifying the response header.

java.lang.UnsupportedOperationException: null
    at org.springframework.http.ReadOnlyHttpHeaders.add(ReadOnlyHttpHeaders.java:67) ~[spring-web-5.1.3.RELEASE.jar:5.1.3.RELEASE]
    at com.oito.gateway.filter.RefreshTokenGlobalFilter.lambda$1(RefreshTokenGlobalFilter.java:63) ~[main/:na]
    at reactor.core.publisher.LambdaMonoSubscriber.onNext(LambdaMonoSubscriber.java:137) ~[reactor-core-3.2.3.RELEASE.jar:3.2.3.RELEASE]
    at org.springframework.cloud.sleuth.instrument.reactor.ScopePassingSpanSubscriber.onNext(ScopePassingSpanSubscriber.java:97) ~[spring-cloud-sleuth-core-2.1.0.RC3.jar:2.1.0.RC3]
    at reactor.core.publisher.Operators$MonoSubscriber.complete(Operators.java:1476) ~[reactor-core-3.2.3.RELEASE.jar:3.2.3.RELEASE]
    at reactor.core.publisher.MonoFlatMap$FlatMapInner.onNext(MonoFlatMap.java:241) ~[reactor-core-3.2.3.RELEASE.jar:3.2.3.RELEASE]
    at reactor.core.publisher.Operators$MonoSubscriber.complete(Operators.java:1476) ~[reactor-core-3.2.3.RELEASE.jar:3.2.3.RELEASE]
    at reactor.core.publisher.MonoSingle$SingleSubscriber.onComplete(MonoSingle.java:171) ~[reactor-core-3.2.3.RELEASE.jar:3.2.3.RELEASE]
    at reactor.core.publisher.FluxMap$MapSubscriber.onComplete(FluxMap.java:136) ~[reactor-core-3.2.3.RELEASE.jar:3.2.3.RELEASE]
    at reactor.core.publisher.FluxFlatMap$FlatMapMain.checkTerminated(FluxFlatMap.java:794) ~[reactor-core-3.2.3.RELEASE.jar:3.2.3.RELEASE]
    at reactor.core.publisher.FluxFlatMap$FlatMapMain.drainLoop(FluxFlatMap.java:560) ~[reactor-core-3.2.3.RELEASE.jar:3.2.3.RELEASE]
    at reactor.core.publisher.FluxFlatMap$FlatMapMain.drain(FluxFlatMap.java:540) ~[reactor-core-3.2.3.RELEASE.jar:3.2.3.RELEASE]
    at reactor.core.publisher.FluxFlatMap$FlatMapMain.onComplete(FluxFlatMap.java:426) ~[reactor-core-3.2.3.RELEASE.jar:3.2.3.RELEASE]
    at reactor.core.publisher.DrainUtils.postCompleteDrain(DrainUtils.java:131) ~[reactor-core-3.2.3.RELEASE.jar:3.2.3.RELEASE]
    at reactor.core.publisher.DrainUtils.postComplete(DrainUtils.java:186) ~[reactor-core-3.2.3.RELEASE.jar:3.2.3.RELEASE]
    at reactor.core.publisher.FluxMapSignal$FluxMapSignalSubscriber.onComplete(FluxMapSignal.java:213) ~[reactor-core-3.2.3.RELEASE.jar:3.2.3.RELEASE]
    at org.springframework.cloud.sleuth.instrument.reactor.ScopePassingSpanSubscriber.onComplete(ScopePassingSpanSubscriber.java:113) ~[spring-cloud-sleuth-core-2.1.0.RC3.jar:2.1.0.RC3]
    at reactor.core.publisher.FluxMap$MapSubscriber.onComplete(FluxMap.java:136) ~[reactor-core-3.2.3.RELEASE.jar:3.2.3.RELEASE]
    at reactor.core.publisher.FluxPeek$PeekSubscriber.onComplete(FluxPeek.java:252) ~[reactor-core-3.2.3.RELEASE.jar:3.2.3.RELEASE]
    at reactor.core.publisher.FluxMap$MapSubscriber.onComplete(FluxMap.java:136) ~[reactor-core-3.2.3.RELEASE.jar:3.2.3.RELEASE]
    at reactor.netty.channel.FluxReceive.terminateReceiver(FluxReceive.java:378) ~[reactor-netty-0.8.3.RELEASE.jar:0.8.3.RELEASE]
    at reactor.netty.channel.FluxReceive.drainReceiver(FluxReceive.java:202) ~[reactor-netty-0.8.3.RELEASE.jar:0.8.3.RELEASE]
    at reactor.netty.channel.FluxReceive.onInboundComplete(FluxReceive.java:343) ~[reactor-netty-0.8.3.RELEASE.jar:0.8.3.RELEASE]
    at reactor.netty.channel.ChannelOperations.onInboundComplete(ChannelOperations.java:325) ~[reactor-netty-0.8.3.RELEASE.jar:0.8.3.RELEASE]
    at reactor.netty.channel.ChannelOperations.terminate(ChannelOperations.java:372) ~[reactor-netty-0.8.3.RELEASE.jar:0.8.3.RELEASE]
    at reactor.netty.http.client.HttpClientOperations.onInboundNext(HttpClientOperations.java:511) ~[reactor-netty-0.8.3.RELEASE.jar:0.8.3.RELEASE]
    at reactor.netty.channel.ChannelOperationsHandler.channelRead(ChannelOperationsHandler.java:141) ~[reactor-netty-0.8.3.RELEASE.jar:0.8.3.RELEASE]
    at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:362) ~[netty-transport-4.1.31.Final.jar:4.1.31.Final]
    at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:348) ~[netty-transport-4.1.31.Final.jar:4.1.31.Final]
    at io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:340) ~[netty-transport-4.1.31.Final.jar:4.1.31.Final]
    at io.netty.handler.codec.MessageToMessageDecoder.channelRead(MessageToMessageDecoder.java:102) ~[netty-codec-4.1.31.Final.jar:4.1.31.Final]
    at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:362) ~[netty-transport-4.1.31.Final.jar:4.1.31.Final]
    at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:348) ~[netty-transport-4.1.31.Final.jar:4.1.31.Final]
    at io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:340) ~[netty-transport-4.1.31.Final.jar:4.1.31.Final]
    at io.netty.channel.CombinedChannelDuplexHandler$DelegatingChannelHandlerContext.fireChannelRead(CombinedChannelDuplexHandler.java:438) ~[netty-transport-4.1.31.Final.jar:4.1.31.Final]
    at io.netty.handler.codec.ByteToMessageDecoder.fireChannelRead(ByteToMessageDecoder.java:323) ~[netty-codec-4.1.31.Final.jar:4.1.31.Final]
    at io.netty.handler.codec.ByteToMessageDecoder.channelRead(ByteToMessageDecoder.java:297) ~[netty-codec-4.1.31.Final.jar:4.1.31.Final]
    at io.netty.channel.CombinedChannelDuplexHandler.channelRead(CombinedChannelDuplexHandler.java:253) ~[netty-transport-4.1.31.Final.jar:4.1.31.Final]
    at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:362) ~[netty-transport-4.1.31.Final.jar:4.1.31.Final]
    at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:348) ~[netty-transport-4.1.31.Final.jar:4.1.31.Final]
    at io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:340) ~[netty-transport-4.1.31.Final.jar:4.1.31.Final]
    at io.netty.channel.DefaultChannelPipeline$HeadContext.channelRead(DefaultChannelPipeline.java:1434) ~[netty-transport-4.1.31.Final.jar:4.1.31.Final]
    at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:362) ~[netty-transport-4.1.31.Final.jar:4.1.31.Final]
    at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:348) ~[netty-transport-4.1.31.Final.jar:4.1.31.Final]
    at io.netty.channel.DefaultChannelPipeline.fireChannelRead(DefaultChannelPipeline.java:965) ~[netty-transport-4.1.31.Final.jar:4.1.31.Final]
    at io.netty.channel.epoll.AbstractEpollStreamChannel$EpollStreamUnsafe.epollInReady(AbstractEpollStreamChannel.java:799) ~[netty-transport-native-epoll-4.1.31.Final-linux-x86_64.jar:4.1.31.Final]
    at io.netty.channel.epoll.EpollEventLoop.processReady(EpollEventLoop.java:433) ~[netty-transport-native-epoll-4.1.31.Final-linux-x86_64.jar:4.1.31.Final]
    at io.netty.channel.epoll.EpollEventLoop.run(EpollEventLoop.java:330) ~[netty-transport-native-epoll-4.1.31.Final-linux-x86_64.jar:4.1.31.Final]
    at io.netty.util.concurrent.SingleThreadEventExecutor$5.run(SingleThreadEventExecutor.java:897) ~[netty-common-4.1.31.Final.jar:4.1.31.Final]
    at java.base/java.lang.Thread.run(Thread.java:834) ~[na:na]

Hi ,
exchange.getResponse().getHeaders() is returning an instance of
io.netty.handler.codec.http.ReadOnlyHttpHeaders.

Contents of the set method in ReadOnlyHttpHeaders is as follows. So I
believe it should also send same exception when we use set.

@Override
public HttpHeaders set(String name, Object value) {
    throw new UnsupportedOperationException("read only");
}

Also we are not relying on any routes configuration(we are using Automated
routes configured for eureka), we cannot use this fure sure.

On Thu, Jan 3, 2019 at 2:03 PM YoungJae Kim notifications@github.com
wrote:

Hi, @jobinvjohn https://github.com/jobinvjohn

you can use set
exchange.getResponse().getHeaders().set(name, value);

or more recommendations way

spring:
cloud:
gateway:
routes:
- id: setresponseheader_route
uri: http://example.org
filters:
- SetResponseHeader=X-Response-Foo, Bar

—
You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub
https://github.com/spring-cloud/spring-cloud-gateway/issues/748#issuecomment-451080720,
or mute the thread
https://github.com/notifications/unsubscribe-auth/AMnpxBJrdLbOVtuzfjmCaG-kl7-0yWnvks5u_cBggaJpZM4ZnlSS
.

java.lang.UnsupportedOperationException: null
at org.springframework.http.ReadOnlyHttpHeaders.set(ReadOnlyHttpHeaders.java:82) ~[spring-web-5.1.3.RELEASE.jar:5.1.3.RELEASE]
at com.oito.gateway.filter.RefreshTokenGlobalFilter.lambda$2(RefreshTokenGlobalFilter.java:58) ~[main/:na]

@Gsealy Thanks for the response. Still i am facing the same issue. Please find the following code that i am using ..

@Configuration
@Slf4j
public class RefreshTokenGlobalFilter implements GlobalFilter, Ordered {

    @Autowired
    @Qualifier(MicroService.AUTH_SERVICE_WEBCLIENT)
    private WebClient authWebClient;

    @Override
    public Mono<Void> filter(final ServerWebExchange exchange, final GatewayFilterChain chain) {
        final var token = JWTUtil.getTokenFromExchange(exchange);
        if (token.isEmpty()) {
            return chain.filter(exchange);
        }
        final var decoratedResponse = new ServerHttpResponseDecorator(exchange.getResponse());
        return chain.filter(exchange).then(Mono.fromRunnable(() -> {

            exchange.getSession().subscribe(session -> {
                final var userContext = (UserContext) session.getAttributes().get("userContext");
                if (userContext != null) {
                    final var refreshToken = userContext.getRefreshToken();

                    if (refreshToken != null && JWTUtil.isTokenExpired(token)) {
                        final Mono<TokenHolderBean> refreshTokenMono = authWebClient.get()
                                .uri("/auth/refresh-token/{refreshToken}", refreshToken)
                                .accept(MediaType.APPLICATION_JSON).retrieve().bodyToMono(TokenHolderBean.class);
                        refreshTokenMono.subscribe(userTokenHolder -> {
                            log.info("Token Expired.. Setting the new Token");
                            decoratedResponse.getHeaders().set("Authorization",
                                    "Bearer " + userTokenHolder.getAccessToken());
                        });
                    }
                }
            });
        }));

    }

    @Override
    public int getOrder() {
        return 1;
    }
}

@jobinvjohn can you attach a complete project as a zip? i can't reproduce it. thanks.

I got the root cause. Getting the refreshTokenMono is webclient call which is in a different service.. By the time it gives the response, main response is already about to commit and wont allow us to modify the response headers. I think i have to go for a blocking call here. Any otherway is there apart from blocking call?

This is working ..

    final var request = exchange.getRequest().mutate().headers(headers -> {
        final var session = exchange.getSession().block();
        final UserContext userContext = session.getAttribute(GatewayConstants.USER_CONTEXT);

        final boolean isGuestUser = isGuestUser(token);
        if (userContext != null && token.equals(userContext.getAccessToken())) {
            final var refreshToken = userContext.getRefreshToken();
            if (refreshToken != null && JWTUtil.isTokenExpired(token)) {
                final TokenHolderBean userTokenHolder = authWebClient.get()
                        .uri("/auth/refresh-token/{refreshToken}", refreshToken).accept(MediaType.APPLICATION_JSON)
                        .retrieve().bodyToMono(TokenHolderBean.class).block(); //Blocking call to hold the exchange.getResponse for not  becoming in a readonly state
                userContext.setIdToken(userTokenHolder.getIdToken());
                log.info("Token Expired.. Setting the new Token");
                exchange.getResponse().getHeaders().add("Authorization",
                        "Bearer " + userTokenHolder.getAccessToken());
            }
            final var idToken = userContext.getIdToken();
            logSessionId(exchange, isGuestUser, session, idToken);
            addToHeader(headers, GatewayConstants.USER_CONTEXT, new ObjectMapper().valueToTree(userContext));
        } 
    }).build();

just wanted to know anyway to avoid blocking call in above code ?

I think there are running into a race condition. you can use AtomicReference with subscribe() to get session content.

AtomicReference<String> sessionRef = new AtomicReference<>();
exchange.getSession().subscribe(session-> {
  sessionRef.set(session.getAttribute(GatewayConstants.USER_CONTEXT));
});
final UserContext userContext = sessionRef.get();

if userContext is null you only can get session with map() , like this

exchange.getSession()
          .map(webSession -> (String) webSession.getAttribute(GatewayConstants.USER_CONTEXT))
          // next step handle it
          ;

you can test it. because I can't reproduce it with you given code.

The following should work.

exchange.getResponse().getHeaders().add(config.getName(), config.getValue());

You are referencing the response before the response has returned.

        final var decoratedResponse = new ServerHttpResponseDecorator(exchange.getResponse());

Needs to go after

        return chain.filter(exchange).then(Mono.fromRunnable(() -> {

Also, calling subscribe() on the session probably isn't the way to go. Better to use a map() and let webflux handle the subscription`.

Try this

         ServerHttpResponse response = exchange.getResponse();
         response.beforeCommit(() -> {
                     response.getHeaders().set(key,value));
             }
             return Mono.empty();
         });

why close?

@junneyang Refer to my reply above, it can solve the problem.

@sanxun0325 did you use .beforeCommit inside a doFinally block?

Was this page helpful?
0 / 5 - 0 ratings

Related issues

larva2333 picture larva2333  Â·  6Comments

ryanjbaxter picture ryanjbaxter  Â·  6Comments

bijukunjummen picture bijukunjummen  Â·  3Comments

manishonline picture manishonline  Â·  3Comments

samtonyclarke picture samtonyclarke  Â·  3Comments