Search code examples
javaspring-bootspring-cloudspring-cloud-gateway

How to set request body in GatewayFilter with Spring Cloud Gateway


i have a GatewayFilter.It receives a request,

  1. checks the signature first (code omitted)
  2. queries some required parameters
  3. adds them to the request
  4. finally forwards the request

How do I add the query parameters to the request body as json?

public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain)  {
        ServerHttpResponse response = exchange.getResponse();
        ServerHttpRequest request = exchange.getRequest();



        BodyInserter<Mono<String>, ReactiveHttpOutputMessage> bodyInserter = getBodyInsert(exchange);
        CachedBodyOutputMessage outputMessage = getCachedBodyOutputMessage(exchange);
        
        return bodyInserter.insert(outputMessage, new BodyInserterContext()).then(Mono.defer(() -> {
           //1.checks the signature  (omit)
    
            //2. get these parameters remotely
            Map<String, Object> paramMap = new HashMap<>();
            paramMap.put("calcPackageId", 10000);
            paramMap.put("userId", 10001);
            paramMap.put("barId", 10002);
            String body = JSON.toJSON(paramMap);
            //3. todo how to set request body {"calcPackageId":"10000","barId":"10002","userId":"10001"} ??????

          //4 forwards the request
            return chain.filter(exchange.mutate().request(newRequest).build());
        }));
    }



   //not important

    private BodyInserter<Mono<String>, ReactiveHttpOutputMessage> getBodyInsert(ServerWebExchange exchange) {
        ServerRequest serverRequest = ServerRequest.create(exchange, messageReaders);
        Mono<String> rawBody = serverRequest.bodyToMono(String.class).map(s -> s);
        return BodyInserters.fromPublisher(rawBody, String.class);

    }

    private CachedBodyOutputMessage getCachedBodyOutputMessage(ServerWebExchange exchange) {
        HttpHeaders tempHeaders = new HttpHeaders();
        tempHeaders.putAll(exchange.getRequest().getHeaders());
        tempHeaders.remove(HttpHeaders.CONTENT_LENGTH);
        return new CachedBodyOutputMessage(exchange, tempHeaders);
    }


Solution

  • One approach would be injecting a ModifyRequestBodyGatewayFilterFactory bean into you filter. It's automatically initialized by Spring Cloud Gateway's Autoconfig.

    Then, to it's .apply method, you can pass a RewriteFunction object. With this you could just create a custom class that implements the RewriteFunction interface, initialize it with the data you need and then return it as the new body. For example:

    public class SomeFilter implements GatewayFilter {
    
        private final ModifyRequestBodyGatewayFilterFactory factory;
    
        public SomeFilter(ModifyRequestBodyGatewayFilterFactory factory) {
            this.factory = factory;
        }
    
        @Override
        public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
            Map<String, Integer> someMap = new HashMap<>();
            someMap.put("AbC", 100);
    
            ModifyRequestBodyGatewayFilterFactory.Config cfg = new ModifyRequestBodyGatewayFilterFactory.Config();
            cfg.setRewriteFunction(String.class, String.class, new SomeRewriteFunction(someMap));
    
            GatewayFilter modifyBodyFilter = factory.apply(cfg);
    
            return modifyBodyFilter.filter(exchange, ch -> Mono.empty())
                    .then(chain.filter(exchange));
        }
    }
    

    and the rewrite function:

    public class SomeRewriteFunction implements RewriteFunction<String, String> {
    
        private final Map<String, Integer> values;
    
        public SomeRewriteFunction(Map<String, Integer> values) {
            this.values = values;
        }
    
        @Override
        public Publisher<String> apply(ServerWebExchange serverWebExchange, String oldBody) {
            /* do things here */
            /* example: */
            try {
                String newBody = new ObjectMapper().writeValueAsString(values);
                return Mono.just(newBody);
            } catch (Exception e) {
                /* error parsing values to json, do something else */
                return Mono.just(oldBody);
            }
        }
    }
    

    Other approach is to inject a decorator and then modify its getBody() method:

    public class SomeFilter implements GatewayFilter {
    
        @Override
        public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
            Map<String, Integer> someMap = new HashMap<>();
            someMap.put("AbC", 100);
    
            SomeDecorator requestDecorator = new SomeDecorator(exchange.getRequest(), someMap);
    
            return chain.filter(exchange.mutate().request(requestDecorator).build());
        }
    }
    

    and the decorator:

    public class SomeDecorator extends ServerHttpRequestDecorator {
    
        private final Map<String, Integer> values;
    
        public SomeDecorator(ServerHttpRequest delegate, Map<String, Integer> values) {
            super(delegate);
            this.values = values;
        }
    
        @Override
        public Flux<DataBuffer> getBody() {
    
            try {
                String newBody = new ObjectMapper().writeValueAsString(values);
                DefaultDataBufferFactory factory = new DefaultDataBufferFactory();
                DefaultDataBuffer buffer = factory.wrap(newBody.getBytes());
                return Flux.just(buffer);
            } catch (JsonProcessingException e) {
                return super.getBody();
            }
        }
    }