Search code examples
spring-cloudspring-cloud-gateway

Only apply modifyResponseBody for certain content-type


I am using GatewayFilterSpec.modifyResponseBody (marked as a "BETA" feature) to rewrite JSON payloads. This works well as long as the response payloads are in fact of content-type application/json. In my case, that is unfortunately not always guaranteed, and I would like it to only apply the modifyResponseBody if the reponse has the Content-Type: application/json header, else skip the filter. Is this possible with Spring Cloud Gateway, and how to do this? Thank you.

Now I'm getting this:

org.springframework.web.reactive.function.UnsupportedMediaTypeException: Content type 'text/html' not supported
    at org.springframework.web.reactive.function.BodyInserters.lambda$null$11(BodyInserters.java:329)
    at java.util.Optional.orElseGet(Optional.java:267)
    at org.springframework.web.reactive.function.BodyInserters.lambda$bodyInserterFor$12(BodyInserters.java:325)

Solution

  • Here is a "solution", one that has all sorts of problems:

    package my_package;
    
    import org.reactivestreams.Publisher;
    import org.springframework.cloud.gateway.filter.GatewayFilter;
    import org.springframework.cloud.gateway.filter.GatewayFilterChain;
    import org.springframework.cloud.gateway.filter.factory.rewrite.ModifyResponseBodyGatewayFilterFactory;
    import org.springframework.context.annotation.Primary;
    import org.springframework.core.io.buffer.DataBuffer;
    import org.springframework.http.codec.ServerCodecConfigurer;
    import org.springframework.http.server.reactive.ServerHttpResponse;
    import org.springframework.http.server.reactive.ServerHttpResponseDecorator;
    import org.springframework.stereotype.Component;
    import org.springframework.web.server.ServerWebExchange;
    import reactor.core.publisher.Mono;
    
    import static org.springframework.http.MediaType.APPLICATION_JSON;
    
    @Component
    @Primary
    public class JsonOnlyModifyResponseBodyGatewayFilterFactory extends ModifyResponseBodyGatewayFilterFactory {
        public JsonOnlyModifyResponseBodyGatewayFilterFactory(ServerCodecConfigurer codecConfigurer) {
            super(codecConfigurer);
        }
    
        @Override
        public GatewayFilter apply(Config config) {
            return new MyModifyResponseGatewayFilter(config);
        }
    
        public class MyModifyResponseGatewayFilter extends ModifyResponseGatewayFilter {
            MyModifyResponseGatewayFilter(Config config) {
                super(config);
            }
    
            @Override
            public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
                ServerHttpResponse serverHttpResponse = getServerHttpResponseFromSuper(exchange);
                ServerHttpResponseDecorator responseDecorator = new ServerHttpResponseDecorator(exchange.getResponse()) {
                    @Override
                    public Mono<Void> writeWith(Publisher<? extends DataBuffer> body) {
                        if (APPLICATION_JSON.isCompatibleWith(getDelegate().getHeaders().getContentType())) {
                            return serverHttpResponse.writeWith(body);
                        }
                        return super.writeWith(body);
                    }
                };
                return chain.filter(exchange.mutate().response(responseDecorator).build());
            }
    
            private ServerHttpResponse getServerHttpResponseFromSuper(ServerWebExchange exchange) {
                ServerHttpResponse[] serverHttpResponse = new ServerHttpResponse[1];
                //noinspection UnassignedFluxMonoInstance
                super.filter(exchange, chain -> {
                    serverHttpResponse[0] = chain.getResponse(); // capture the response when the super sets it
                    return null;
                });
                return serverHttpResponse[0];
            }
        }
    }
    

    The chosen approach is in lieu of just changing a copy of the existing ModifyResponseBodyGatewayFilterFactory. This allows version upgrades of Spring Boot Gateway to bring in minor changes of ModifyResponseBodyGatewayFilterFactory. But since JsonOnlyModifyResponseBodyGatewayFilterFactory is very dependent on the implementation of ModifyResponseBodyGatewayFilterFactory, this may easily get broken. Another flaw of this solution is that I had to put an @Primary annotation to avoid a required a single bean, but 2 were found exception, but it overrides the default which would presumably affect other uses of modifyResponseBody. It's ugly to call super.filter and not use its result. And so on. So, while this "works", it doesn't, well, fill me with joy.