2

I have spent the last 2 days trying every possible way of modifying the response body of a request before it hits the client, and nothing seems to work for me. So far I have tried the implementations mentioned here, here, here, here, here and a few others that I can't find right now, but nothing has worked. It doesn't matter if I define the filter as pre, post, global, gateway or route-specific - the actual response modification doesn't seem to work for me.

My situation is the following: I have a YAML-configured API gateway running and have configured one of its routes to lead to an ADF service in the background. The issue I have with this ADF application is that the response it returns to the client is in the form of an HTML template that is automatically generated by its backend. In this template, some of the URLs are hardcoded and point to the address of the application itself. To justify the use of an API Gateway in this case, I want to replace those ADF URLs with those of the API Gateway.

For simplicity's sake, let's say the IP address of my ADF service is 1.2.3.4:1234, and the IP address of my API Gateway is localhost:8080. When I hit the ADF route in my gateway, the response contains some auto-generated javascript inserts, such as this one:

AdfPage.PAGE.__initializeSessionTimeoutTimer(1800000, 120000, "http://1.2.3.4:1234/entry/dynamic/index.jspx");

As you can see, it contains a hardcoded URL. I want to access the response body and find all those hardcoded URLs and replace them with the gateway URL, so the above example becomes:

AdfPage.PAGE.__initializeSessionTimeoutTimer(1800000, 120000, "http://localhost:8080/entry/dynamic/index.jspx");

To do this, it seems sensible to me to have a global POST filter that kicks in only when the request matches the route for my ADF application, so that's what I've settled on doing.

Here is my post filter so far:

        @Bean
    public GlobalFilter globalADFUrlReplacementFilter() {
        return (exchange, chain) -> chain.filter(exchange).then(Mono.just(exchange)).map(serverWebExchange -> {
                    ServerHttpRequest request = exchange.getRequest();
                    ServerHttpResponse response = exchange.getResponse();

                    if (requestIsTowardsADF(request)) {
                        logger.info("EXECUTING GLOBAL POST FILTER FOR ADF TEMPLATE URL REPLACEMENT");
                        ServerHttpResponseDecorator responseDecorator = new ServerHttpResponseDecorator(response) {

                            @Override
                            @SuppressWarnings("unchecked")
                            public Mono<Void> writeWith(Publisher<? extends DataBuffer> body) {
                                logger.info("OVERRIDING writeWith METHOD TO MODIFY THE BODY");
                                Flux<? extends DataBuffer> flux = (Flux<? extends DataBuffer>) body;
                                return super.writeWith(flux.buffer().map(buffer -> {

                                    DataBufferFactory dataBufferFactory = new DefaultDataBufferFactory();
                                    DataBuffer join = dataBufferFactory.join(buffer);
                                    byte[] content = new byte[join.readableByteCount()];
                                    join.read(content);
                                    DataBufferUtils.release(join);

                                    String bodyStr = new String(content, StandardCharsets.UTF_8);
                                    bodyStr = bodyStr.replace(ADF_URL, API_GATEWAY_URL);

                                    getDelegate().getHeaders().setContentLength(bodyStr.getBytes().length);
                                    return bufferFactory().wrap(bodyStr.getBytes());
                                }));
                            }
                        };
                        logger.info("ADF URL REPLACEMENT FILTER DONE");
                        return chain.filter(serverWebExchange.mutate().request(request).response(responseDecorator).build());
                    }
                    return serverWebExchange;
                })
                .then();
    }

And the config:

spring:
  cloud:
    gateway:
      routes:
        - id: adf-test-2
          uri: http://1.2.3.4:1234
          predicates:
            - Path=/entry/**

You can see that I'm using a org.slf4j.Logger object to log messages in the console. When I run my API Gateway and hit the ADF route, I can see the following:

EXECUTING GLOBAL POST FILTER FOR ADF TEMPLATE URL REPLACEMENT
ADF URL REPLACEMENT FILTER DONE

And when I check the response I got back from the API Gateway, I can see that the response body is still identical and the ADF URLs have not been replaced at all. I tried debugging the application and as soon as it reaches ServerHttpResponseDecorator responseDecorator = new ServerHttpResponseDecorator(response) { it skips over the entire anonymous class implementation within those curly braces. A testament to that is the absence of the OVERRIDING writeWith METHOD TO MODIFY THE BODY log in the console - it never got executed!

It seems that for some reason the actual body modification doesn't get executed and I can't figure out why. I tried several different implementations of this filter, as mentioned in the above links, and neither of them worked.

Can someone please share with me a working POST filter that modifies the response body, or point out the flaw in my solution?

Thanks a bunch in advance!

Hristo Naydenov
  • 113
  • 3
  • 13

2 Answers2

4

Thanks for sharing this sample filter cdan. I provided the most straightforward solution to my issue using it as a template. Here's how it looks:

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory;
import org.springframework.cloud.gateway.filter.factory.rewrite.ModifyResponseBodyGatewayFilterFactory;
import org.springframework.stereotype.Component;
import reactor.core.publisher.Mono;

import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;

@Component
public class TestFilter2 extends AbstractGatewayFilterFactory<TestFilter2.Config> {

    public static final String ADF_URL = "1.2.3.4:1234";
    public static final String AG_URL = "localhost:8080";
    final Logger logger = LoggerFactory.getLogger(TestFilter2.class);

    public static class Config {
        private String param1;

        public Config() {
        }

        public void setParam1(String param1) {
            this.param1 = param1;
        }

        public String getParam1() {
            return param1;
        }
    }

    @Override
    public List<String> shortcutFieldOrder() {
        return List.of("param1");
    }

    private final ModifyResponseBodyGatewayFilterFactory modifyResponseBodyFilterFactory;

    public TestFilter2() {
        super(Config.class);
        this.modifyResponseBodyFilterFactory = new ModifyResponseBodyGatewayFilterFactory(new ArrayList<>(), new HashSet<>(), new HashSet<>());
    }

    @Override
    public GatewayFilter apply(Config config) {
        final ModifyResponseBodyGatewayFilterFactory.Config modifyResponseBodyFilterFactoryConfig = new ModifyResponseBodyGatewayFilterFactory.Config();

        modifyResponseBodyFilterFactoryConfig.setRewriteFunction(String.class, String.class, (exchange, bodyAsString) -> Mono.just(bodyAsString.replace(ADF_URL, AG_URL)));

        return modifyResponseBodyFilterFactory.apply(modifyResponseBodyFilterFactoryConfig);
    }

}

I have added this filter to my route definition like so:

spring:
  cloud:
    gateway:
      httpclient:
        wiretap: true
      httpserver:
        wiretap: true
      routes:
        - id: adf-test-2
          uri: http://1.2.3.4:1234
          predicates:
            - Path=/entry/**
          filters:
            - TestFilter2

I'm simply trying to modify the response body and replace the ADF URL in it with the AG URL, but whenever I try to hit the ADF route I get the below exception:

2022-05-08 17:35:19.492 ERROR 87216 --- [ctor-http-nio-3] a.w.r.e.AbstractErrorWebExceptionHandler : [284b180d-1]  500 Server Error for HTTP GET "/entry/dynamic/index.jspx"

org.springframework.web.reactive.function.UnsupportedMediaTypeException: Content type 'text/html' not supported for bodyType=java.lang.String
    at org.springframework.web.reactive.function.BodyExtractors.lambda$readWithMessageReaders$12(BodyExtractors.java:201) ~[spring-webflux-5.3.18.jar:5.3.18]
    Suppressed: reactor.core.publisher.FluxOnAssembly$OnAssemblyException: 
Error has been observed at the following site(s):
    *__checkpoint ? Body from UNKNOWN  [DefaultClientResponse]
    *__checkpoint ? org.springframework.cloud.gateway.filter.WeightCalculatorWebFilter [DefaultWebFilterChain]
    *__checkpoint ? HTTP GET "/entry/dynamic/index.jspx" [ExceptionHandlingWebHandler]

I searched the web for some time but wasn't able to find any clear answer on why this UnsupportedMediaTypeException: Content type 'text/html' not supported for bodyType=java.lang.String exception gets thrown when I try to work with the bodyAsString field that is supposed to contain the response body as String. Debugging the entire filter didn't work either, as the exception seems to be thrown immediately after I hit the route and I can't even get in the body of that class. Am I missing something obvious?

UPDATE (09.05.2022): After looking into this further, I refactored the filter structure a bit by removing the unnecessary parameter in the config, and Autowiring the dependency towards ModifyResponseBodyGatewayFilterFactory, and now it seems the filter works properly and does the replacement I needed it to do. I will test it a bit longer to make sure it does indeed work as expected, and if it does, I'll mark this as the solution. Thanks for all of your input cdan!

Here's the entire filter:

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory;
import org.springframework.cloud.gateway.filter.factory.rewrite.ModifyResponseBodyGatewayFilterFactory;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Component;
import reactor.core.publisher.Mono;

@Component
public class TestFilter2 extends AbstractGatewayFilterFactory<TestFilter2.Config> {

    public static final String ADF_URL = "1.2.3.4:1234";
    public static final String AG_URL = "localhost:8080";
    @Autowired
    private final ModifyResponseBodyGatewayFilterFactory modifyResponseBodyFilterFactory;

    public static class Config {
        public Config() {
        }
    }

    public TestFilter2(ModifyResponseBodyGatewayFilterFactory modifyResponseBodyFilterFactory) {
        super(Config.class);
        this.modifyResponseBodyFilterFactory = modifyResponseBodyFilterFactory;
    }

    @Override
    public GatewayFilter apply(Config config) {
        final ModifyResponseBodyGatewayFilterFactory.Config modifyResponseBodyFilterFactoryConfig = new ModifyResponseBodyGatewayFilterFactory.Config();
        modifyResponseBodyFilterFactoryConfig.setRewriteFunction(String.class, String.class, (exchange, bodyAsString) -> Mono.just(bodyAsString.replace(ADF_URL, AG_URL)));
        return modifyResponseBodyFilterFactory.apply(modifyResponseBodyFilterFactoryConfig);
    }
}
Hristo Naydenov
  • 113
  • 3
  • 13
  • Maybe my mistake, I was missing the line that does `setNewContentType` when I copy-pasted from my code (probably because I was not sure why it was needed or what content type you would use): `modifyResponseBodyFilterFactoryConfig.setNewContentType(MediaType.TEXT_HTML_VALUE);` I updated my answer with this new line of code. Could you do the same and try again? – cdan May 08 '22 at 17:48
  • How to judge the Content-Type before the text replace ? I found some binaryfile 'file corruption' after text replace. Is there a way to skip it like exec 'chain.filter(exchange)' ? My question : https://stackoverflow.com/questions/73467386/spring-gateway-how-to-get-originresponseheaders-and-replace-proxy-html-content – zy_sun Aug 25 '22 at 06:54
0

Try with the built-in ModifyResponseBody Filter with Java DSL. If you still need more advanced response processing, your next option is to extend the ModifyResponseBodyGatewayFilterFactory class.

(Update 2022-05-08) For example, using the Delegation design pattern (wrapping the built-in ModifyResponseBodyFilter in a new custom filter taking one custom parameter):

package test;

import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory;
import org.springframework.cloud.gateway.filter.factory.rewrite.ModifyResponseBodyGatewayFilterFactory;
import org.springframework.stereotype.Component;
import reactor.core.publisher.Mono;
import java.util.*;


@Component
public class MyFilterFactory extends AbstractGatewayFilterFactory<MyFilterFactory.Config>
{

  public static class Config 
  {
        private String param1;
        // Add other parameters if necessary

        public Config() {}

        public void setParam1(String param1) {
            this.param1 = param1;
        }
 
        public String getParam1() {
            return param1;
        }

        // Add getters and setters for other parameters if any
  }

  @Override
  public List<String> shortcutFieldOrder()
  {
    return Arrays.asList("param1" /*, other parameters */ );
  }

  private final ModifyResponseBodyGatewayFilterFactory modifyResponseBodyFilterFactory;

  public MyFilterFactory() 
  {
        super(Config.class);
        this.modifyResponseBodyFilterFactory = new ModifyResponseBodyGatewayFilterFactory(new ArrayList<>(), new HashSet<>(), new HashSet<>());
  }

  @Override
  public GatewayFilter apply(Config config) 
  {
        final ModifyResponseBodyGatewayFilterFactory.Config modifyResponseBodyFilterFactoryConfig = new ModifyResponseBodyGatewayFilterFactory.Config();
        modifyResponseBodyFilterFactoryConfig.setNewContentType(MediaType.TEXT_HTML_VALUE);
        modifyResponseBodyFilterFactoryConfig.setRewriteFunction(String.class, String.class, (exchange, bodyAsString) -> {

            final String output;
            /*
            Do whatever transformation of bodyAsString (response body as String) and assign the result to output...
             */
            return Mono.just(output);
        });

        return modifyResponseBodyFilterFactory.apply(modifyResponseBodyFilterFactoryConfig);
  }

}
cdan
  • 3,470
  • 13
  • 27
  • Unfortunately using Java DSL is a no-go in my case, as we need to stick with a YAML configuration for now. And regarding the second option - I tried doing that, but quite unsuccessfully. Could you please provide a working example of that solution so I can relate it to mine and see where my mistakes were? I'm just not sure how exactly to extend it and which methods I need to override, in what way, to make it work properly, and examples online are very limited. – Hristo Naydenov May 07 '22 at 13:56
  • OK I added an example of custom filter extending / wrapping the built-in ModifyResponseBody filter to the answer. – cdan May 08 '22 at 04:00
  • Thank you for this cdan! I feel like I'm almost there, but I'm experiencing an odd exception that I can't figure out the reason for. Please see my answer below, and I would very much appreciate your insight on why it's happening. Thanks a lot! – Hristo Naydenov May 08 '22 at 14:49
  • Maybe my mistake, I was missing the line that does `setNewContentType` when I copy-pasted from my code (probably because I was not sure why it was needed or what content type you would use): `modifyResponseBodyFilterFactoryConfig.setNewContentType(MediaType.TEXT_HTML_VALUE);` I updated my answer with this new line of code. Could you do the same and try again? – cdan May 08 '22 at 17:51
  • Ah, I figured it could have something to do with this, but when I tried it there was no change, so I abandoned it as an idea. Now I tried it again, exactly as you have added it to your code, but the issue persists, even after completely rebuilding the project from scratch (just to make sure there's no cached code anywhere that could alter the output). Does this filter work as expected on your end? I must be missing something... – Hristo Naydenov May 09 '22 at 07:11
  • I've updated the filter in my answer below and it seems to work properly when the dependency is autowired. Appreciate your valuable insight on this issue cdan! – Hristo Naydenov May 09 '22 at 07:40
  • Glad you made it! Which version of Spring Cloud Gateway are you using btw? – cdan May 09 '22 at 08:39
  • I'm using 3.1.1 – Hristo Naydenov May 09 '22 at 09:42