As of this writing, there's a somewhat limited/restrictive means of applying HTTP response transformations/modifications via Spring Cloud Gateway, probably because it needs to accommodate both the Mono
and Flux
(aka "reactive") models. The main issue for me is that the only way to configure transformation via the official filter is through Java DSL--i.e.,you cannot configure via application.yml
because the transformation part is Java:
@Bean
public RouteLocator routes(RouteLocatorBuilder builder) {
return builder.routes()
.route("rewrite_response_upper", r -> r.host("*.rewriteresponseupper.org")
.filters(f -> f.prefixPath("/httpbin")
.modifyResponseBody(String.class, String.class,
(exchange, s) -> Mono.just(s.toUpperCase()))).uri(uri)
.build();
}
There's nothing wrong this and it certainly address simple use cases, but the other limitation (I think) is that it only supports Mono
. In this solution, I will present a way to create a custom filter that modifies the response, but it would still be support Mono
but at least you are not restricted to only using Java DSL to configure your routes.
This is basically to look at what the ModifyResponseBodyGatewayFilterFactory
filter does and, um, modify it for our purpose.
In ModifyResponseBodyGatewayFilterFactory
there are 3 key fields in there from the Config
that deals with the transformation: the inClass
, the outClass
and the rewriteFunction
. I'm not going to elaborate too much on the inClass
and outClass
but basically these are the POJOs to deal with the input and output classes of the Response body itself...and in this case, it's just String.class
for both since we are just modifying the JSON response.
The rewriteFunction
is basically the Mono<T>
lambda that applies the input of type inClass
to generate output of type outClass
.
So the quick and dirty solution basically is just to copy almost everything that ModifyResponseBodyGatewayFilterFactory
does and change a few things. Note that some of the ideas presented here are aped from this blog entry
First is to set the inClass and outClass to String.class
Class inClass = String.class;
Class outClass = String.class;
Next, create the constructor that takes a Set<MessageBodyDecoder>
and Set<MessageBodyEncoder>
and assign those to private fields. Again, not going to elaborate too much on this other than it deals with the decoders in case the response goes through other encoders prior to goin to our filter.
private final Map<String, MessageBodyDecoder> messageBodyDecoders;
private final Map<String, MessageBodyEncoder> messageBodyEncoders;
public DynamicPostTransformFilter(Set<MessageBodyDecoder> messageBodyDecoders,
Set<MessageBodyEncoder> messageBodyEncoders) {
super(Config.class);
this.messageBodyDecoders = messageBodyDecoders.stream()
.collect(Collectors.toMap(MessageBodyDecoder::encodingType, identity()));
this.messageBodyEncoders = messageBodyEncoders.stream()
.collect(Collectors.toMap(MessageBodyEncoder::encodingType, identity()));
}
Next is to modify prepareClientResponse
to look like the one below (it changes the third parameter to use the default HttpMessageReader
s):
private ClientResponse prepareClientResponse(Publisher<? extends DataBuffer> body, HttpHeaders httpHeaders) {
ClientResponse.Builder builder;
builder = ClientResponse.create(exchange.getResponse().getStatusCode(), HandlerStrategies.withDefaults().messageReaders());
return builder.headers(headers -> headers.putAll(httpHeaders)).body(Flux.from(body)).build();
}
Now the final key here is define your transformation in applyTransform
which in this case just converts the input to UPPERCASE:
protected String applyTransform(String input, Config config) {
// we're not doing anything fancy here
return input.toUpperCase();
}
and then apply the lambda in the modifiedBody
:
Mono modifiedBody = extractBody(exchange, clientResponse, inClass)
.flatMap(originalBody -> Mono.just(applyTransform((String) originalBody, config))
.switchIfEmpty(Mono.empty());
Many thanks for the exploring solution. finally, fixed the issue after struggling for 3 days for my use case.