3

I have a spring cloud gateway which forwards the API rest requests to some microservices.

I would like to cache the response for specific requests. For this reason I wrote this Filter

@Component
@Slf4j
public class CacheResponseGatewayFilterFactory extends AbstractGatewayFilterFactory<CacheResponseGatewayFilterFactory.Config> {
   private final CacheManager cacheManager;

   public CacheResponseGatewayFilterFactory(CacheManager cacheManager) {
     super(CacheResponseGatewayFilterFactory.Config.class);
     this.cacheManager = cacheManager;
   }

   @Override
   public GatewayFilter apply(CacheResponseGatewayFilterFactory.Config config) {
     final var cache = cacheManager.getCache("MyCache");
     return (exchange, chain) -> {
        final var path = exchange.getRequest().getPath();
        if (nonNull(cache.get(path))) {
            log.info("Return cached response for request: {}", path);
            final var response = cache.get(path, ServerHttpResponse.class);
            final var mutatedExchange = exchange.mutate().response(response).build();
            return mutatedExchange.getResponse().setComplete();
        }

        return chain.filter(exchange).doOnSuccess(aVoid -> {
            cache.put(path, exchange.getResponse());
        });
    };
}

When I call my rest endpoint, the first time I receive the right json, the second time I got an empty body.

What am I doing wrong?

EDIT This is a screenshot of the exchange.getRequest() just before doing cache.put()

enter image description here

Fabry
  • 1,498
  • 4
  • 23
  • 47
  • 1
    have you debugged the cache put? Are you actually caching the response or has it already been sent to the client at that stage and empty? – Darren Forsythe Dec 29 '20 at 20:03
  • @DarrenForsythe I tried to debug it but honestly I didn't get it. I've added a screenshot. If the request has aready been sent to the client, how can I cache it just before sending it? Thanks – Fabry Dec 29 '20 at 20:12
  • Looks like you can use a [decorator](https://github.com/spring-cloud/spring-cloud-gateway/issues/268) to copy into the cache – Ben Manes Dec 29 '20 at 20:17
  • @BenManes I saw it but the code about the decorator is incomplete and I didn't get how I can cache the result. Maybe you have a more complete example? – Fabry Dec 29 '20 at 20:48
  • On the request side, they have a [caching utility](https://github.com/spring-cloud/spring-cloud-gateway/blob/c19af741aa9efec5cd7bf351a77e8ee2f648b228/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/support/ServerWebExchangeUtils.java#L318-L341) which might be instructive. Similarly, a encrypt/decrypt [example](https://levelup.gitconnected.com/spring-cloud-gateway-encryption-decryption-of-request-response-4e76f5b15718) might be adaptable to a cache? I'm not very familiar with Spring, so hope that helps. – Ben Manes Dec 29 '20 at 21:04
  • You appear to be saving on success of the chain, rather than during it. I would guess the reponse has already been sent to the client and whatever wrapper for the content is gone. That response also looks stateful with all the things in the fields. I'd suggest just caching the body (and headers if you must) and doing it and muating the response with the cache body if it exited. I think you will need to `map` and recreate a mutate a response regardless when needing to cache as again I think you can only read the response once . Caching a – Darren Forsythe Dec 29 '20 at 21:26

1 Answers1

6

I solved it creating a GlobalFilter and a ServerHttpResponseDecorator. This code is caching all the responses regardless (it can be easily improved to cache only specific responses).

This is the code. However I think it can be improved. In case let me know.

@Slf4j
@Component
public class CacheFilter implements GlobalFilter, Ordered {
  private final CacheManager cacheManager;

  public CacheFilter(CacheManager cacheManager) {
    this.cacheManager = cacheManager;
  }

  @Override
  public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
    final var cache = cacheManager.getCache("MyCache");

    final var cachedRequest = getCachedRequest(exchange.getRequest());
    if (nonNull(cache.get(cachedRequest))) {
        log.info("Return cached response for request: {}", cachedRequest);
        final var cachedResponse = cache.get(cachedRequest, CachedResponse.class);

        final var serverHttpResponse = exchange.getResponse();
        serverHttpResponse.setStatusCode(cachedResponse.httpStatus);
        serverHttpResponse.getHeaders().addAll(cachedResponse.headers);
        final var buffer = exchange.getResponse().bufferFactory().wrap(cachedResponse.body);
        return exchange.getResponse().writeWith(Flux.just(buffer));
    }

    final var mutatedHttpResponse = getServerHttpResponse(exchange, cache, cachedRequest);
    return chain.filter(exchange.mutate().response(mutatedHttpResponse).build());
  }

  private ServerHttpResponse getServerHttpResponse(ServerWebExchange exchange, Cache cache, CachedRequest cachedRequest) {
    final var originalResponse = exchange.getResponse();
    final var dataBufferFactory = originalResponse.bufferFactory();

    return new ServerHttpResponseDecorator(originalResponse) {

        @NonNull
        @Override
        public Mono<Void> writeWith(@NonNull Publisher<? extends DataBuffer> body) {
            if (body instanceof Flux) {
                final var flux = (Flux<? extends DataBuffer>) body;
                return super.writeWith(flux.buffer().map(dataBuffers -> {
                    final var outputStream = new ByteArrayOutputStream();
                    dataBuffers.forEach(dataBuffer -> {
                        final var responseContent = new byte[dataBuffer.readableByteCount()];
                        dataBuffer.read(responseContent);
                        try {
                            outputStream.write(responseContent);
                        } catch (IOException e) {
                            throw new RuntimeException("Error while reading response stream", e);
                        }
                    });
                    if (Objects.requireNonNull(getStatusCode()).is2xxSuccessful()) {
                        final var cachedResponse = new CachedResponse(getStatusCode(), getHeaders(), outputStream.toByteArray());
                        log.debug("Request {} Cached response {}", cacheKey.getPath(), new String(cachedResponse.getBody(), UTF_8));
                        cache.put(cacheKey, cachedResponse);
                    }
                    return dataBufferFactory.wrap(outputStream.toByteArray());
                }));
            }
            return super.writeWith(body);
        }
    };
  }

  @Override
  public int getOrder() {
    return -2;
  }

  private CachedRequest getCachedRequest(ServerHttpRequest request) {
    return CachedRequest.builder()
            .method(request.getMethod())
            .path(request.getPath())
            .queryParams(request.getQueryParams())
            .build();
  }

  @Value
  @Builder
  private static class CachedRequest {
    RequestPath path;
    HttpMethod method;
    MultiValueMap<String, String> queryParams;

  }

  @Value
  private static class CachedResponse {
    HttpStatus httpStatus;
    HttpHeaders headers;
    byte[] body;
  }
}
Fabry
  • 1,498
  • 4
  • 23
  • 47