Spring-cloud-gateway: Provide A Way To Simplify Reading And Caching A Body In A Filter or Predicate

Created on 25 Mar 2019  路  16Comments  路  Source: spring-cloud/spring-cloud-gateway

There are already multiple issues(#135 #152 #502) regarding reading request body or form data from original request multiple times. The default answer to that use case is implementing caching of getBody for NettyRoutingFilter to work, since getBody allows only one request to succeed, all following requests fail with java.lang.IllegalStateException: Only one connection receive subscriber allowed. Also due to this limitation HiddenHttpMethodFilter was disabled in Gateway.

I think that having to implement body caching is error prone and not very trivial especially for novices in reactive, and since there were already a lot of questions regarding this issue maybe it makes sense to add such global filter into Gateway out of the box?

enhancement

Most helpful comment

This might not be the most efficient, but other solutions I have tried using retain/slice along with discard seem to leak.

This appears pretty stable. We read the body into memory if it exists, and we decorate with a newly created buffer. If we retry/repeat. We will have an untouched buffer with a ref count of 1.

```@Component
public class CopyBodyGlobalFilter implements GlobalFilter, Ordered {

public static final String CUSTOM_CACHED_REQUEST_BODY_KEY = "customCachedRequestBody";

@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
    Object body = exchange.getAttributeOrDefault(CUSTOM_CACHED_REQUEST_BODY_KEY,
            null);

    if (body != null) {
        return chain.filter(exchange);
    }

    return DataBufferUtils.join(exchange.getRequest().getBody())
            .map(dataBuffer -> {
                byte[] bytes = new byte[dataBuffer.readableByteCount()];
                dataBuffer.read(bytes);
                DataBufferUtils.release(dataBuffer);
                return bytes;
            })
            .defaultIfEmpty(new byte[0])
            .doOnNext(bytes -> exchange.getAttributes().put(CUSTOM_CACHED_REQUEST_BODY_KEY, bytes))
            .then(chain.filter(exchange));
}

@Override
public int getOrder() {
    return RouteToRequestUrlFilter.ROUTE_TO_URL_FILTER_ORDER + 1;
}

}

@Component
public class CustomAdaptCachedBodyGlobalFilter implements GlobalFilter, Ordered {
@Override
public Mono filter(ServerWebExchange exchange, GatewayFilterChain chain) {
byte[] body = exchange.getAttributeOrDefault(CUSTOM_CACHED_REQUEST_BODY_KEY,
null);
DataBufferFactory dataBufferFactory = exchange.getResponse().bufferFactory();

    if (body != null) {
        ServerHttpRequestDecorator decorator = new ServerHttpRequestDecorator(
                exchange.getRequest()) {
            @Override
            public Flux<DataBuffer> getBody() {
                if (body.length > 0) {
                    return Flux.just(dataBufferFactory.wrap(body));
                }
                return Flux.empty();
            }
        };
        return chain.filter(exchange.mutate().request(decorator).build());
    }
    return chain.filter(exchange);
}

}
```

All 16 comments

This might not be the most efficient, but other solutions I have tried using retain/slice along with discard seem to leak.

This appears pretty stable. We read the body into memory if it exists, and we decorate with a newly created buffer. If we retry/repeat. We will have an untouched buffer with a ref count of 1.

```@Component
public class CopyBodyGlobalFilter implements GlobalFilter, Ordered {

public static final String CUSTOM_CACHED_REQUEST_BODY_KEY = "customCachedRequestBody";

@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
    Object body = exchange.getAttributeOrDefault(CUSTOM_CACHED_REQUEST_BODY_KEY,
            null);

    if (body != null) {
        return chain.filter(exchange);
    }

    return DataBufferUtils.join(exchange.getRequest().getBody())
            .map(dataBuffer -> {
                byte[] bytes = new byte[dataBuffer.readableByteCount()];
                dataBuffer.read(bytes);
                DataBufferUtils.release(dataBuffer);
                return bytes;
            })
            .defaultIfEmpty(new byte[0])
            .doOnNext(bytes -> exchange.getAttributes().put(CUSTOM_CACHED_REQUEST_BODY_KEY, bytes))
            .then(chain.filter(exchange));
}

@Override
public int getOrder() {
    return RouteToRequestUrlFilter.ROUTE_TO_URL_FILTER_ORDER + 1;
}

}

@Component
public class CustomAdaptCachedBodyGlobalFilter implements GlobalFilter, Ordered {
@Override
public Mono filter(ServerWebExchange exchange, GatewayFilterChain chain) {
byte[] body = exchange.getAttributeOrDefault(CUSTOM_CACHED_REQUEST_BODY_KEY,
null);
DataBufferFactory dataBufferFactory = exchange.getResponse().bufferFactory();

    if (body != null) {
        ServerHttpRequestDecorator decorator = new ServerHttpRequestDecorator(
                exchange.getRequest()) {
            @Override
            public Flux<DataBuffer> getBody() {
                if (body.length > 0) {
                    return Flux.just(dataBufferFactory.wrap(body));
                }
                return Flux.empty();
            }
        };
        return chain.filter(exchange.mutate().request(decorator).build());
    }
    return chain.filter(exchange);
}

}
```

This is actually something we were discussing today as a team, it is something we are considering

Do we know when this will be released, we are eagerly waiting for this change request! thanks

Until there is a version assigned, it is not scheduled.

Good Morning.

@spencergibb , have you scheduled a version yet?.

If there was it would be under the Milestone section

@dave-fl
Thanks a lot, dude. You really saved my day. I've spending countless hours to find the answer. Finally, this one works for me. No truncated body, No changes on body transfer, really perfect solution!

@spencergibb @ryanjbaxter where can we find the documentation for the feature that supports this?

@DJLB #1121 is still open. Otherwise checkout ec0d906ff3992789a79ab6e3029374dae478aea8

Thanks @spencergibb . Is it correct to assume this feature is not supported in 2.2.1 for YAML-configureed/non-java-configure routes?

What feature?

@spencergibb Storing/accessing a cached request body object for a routed request (gleaming from discussion about ReadBodyPredicateFactory here https://github.com/spring-cloud/spring-cloud-gateway/issues/690#issuecomment-443690874)

yaml is not supported

@spencergibb thanks, good to know. Is supporting this feature for YAML-configured routes a part of the SCG roadmap? Or should a feature request be made in this case?

Also, regarding YAML support in general鈥攊s YAML-routing config for SCG considered second-class to Java-based config in terms of feature support and future development? Is it being deprecated in any way?

configuration based routing isn't second class at all. I literally can't put a programming language in yaml required to support body-based predicates and filters.

@spencergibb @DJLB Just noting a pretty simple workaround that might work for some people, if all they want is a string form of the body, is to just extend the ReadyBodyPredicateFactory

@Component
public class ReadBodyToStringRoutePredicateFactory extends ReadBodyPredicateFactory {

    @Override
    public AsyncPredicate<ServerWebExchange> applyAsync(Config config) {
        config.setPredicate(String.class, i -> true);
        return super.applyAsync(config);
    }

}

which you can then use in yaml file as :

- ReadBodyToString=
Was this page helpful?
0 / 5 - 0 ratings

Related issues

adrianbrad picture adrianbrad  路  30Comments

re6exp picture re6exp  路  37Comments

dave-fl picture dave-fl  路  32Comments

renanpalmeira picture renanpalmeira  路  30Comments

sincang picture sincang  路  41Comments