1

While migrating my spring server from servlets to reactive I had to change all the filters in the code to WebFilter. One of the filters was decompressing gzipped content, but I couldn't do the same with the new WebFilter.

With servlets I wrapped the inputstream with a GzipInputStream. What is the best practice to do it with spring reactive?

Yuval
  • 764
  • 1
  • 9
  • 23

2 Answers2

0

Solution:

@Component
public class GzipFilter implements WebFilter {

  private static final Logger LOG = LoggerFactory.getLogger(GzipFilter.class);
  public static final String CONTENT_ENCODING = "content-encoding";
  public static final String GZIP = "gzip";
  public static final String UTF_8 = "UTF-8";

  @Override
  public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {

    ServerHttpRequest request = exchange.getRequest();

    if (!isGzip(request)) {
      return chain.filter(exchange);
    }
    else {

      ServerHttpRequest mutatedRequest = new ServerHttpRequestWrapper(request);
      ServerWebExchange mutatedExchange = exchange.mutate().request(mutatedRequest).build();

      return chain.filter(mutatedExchange);
    }
  }

  private boolean isGzip(ServerHttpRequest serverHttpRequest) {
    String encoding = serverHttpRequest.getHeaders().getFirst(CONTENT_ENCODING);

    return encoding != null && encoding.contains(GZIP);
  }


  private static class ServerHttpRequestWrapper implements ServerHttpRequest {

    private ServerHttpRequest request;

    public ServerHttpRequestWrapper(ServerHttpRequest request) {
      this.request = request;
    }

    private static byte[] getDeflatedBytes(GZIPInputStream gzipInputStream) throws IOException {
      StringWriter writer = new StringWriter();
      IOUtils.copy(gzipInputStream, writer, UTF_8);
      return writer.toString().getBytes();
    }


    @Override
    public String getId() {
      return request.getId();
    }

    @Override
    public RequestPath getPath() {
      return request.getPath();
    }

    @Override
    public MultiValueMap<String, String> getQueryParams() {
      return request.getQueryParams();
    }

    @Override
    public MultiValueMap<String, HttpCookie> getCookies() {
      return request.getCookies();
    }

    @Override
    public String getMethodValue() {
      return request.getMethodValue();
    }

    @Override
    public URI getURI() {
      return request.getURI();
    }

    @Override
    public Flux<DataBuffer> getBody() {

      Mono<DataBuffer> mono = request.getBody()
          .map(dataBuffer -> dataBuffer.asInputStream(true))
          .reduce(SequenceInputStream::new)
          .map(inputStream -> {
            try (GZIPInputStream gzipInputStream = new GZIPInputStream(inputStream)) {
              byte[] targetArray = getDeflatedBytes(gzipInputStream);
              return new DefaultDataBufferFactory().wrap(targetArray);
            }
            catch (IOException e) {
              throw new IllegalGzipRequest(String.format("failed to decompress gzip content. Path: %s", request.getPath()));
            }
          });

      return mono.flux();
    }

    @Override
    public HttpHeaders getHeaders() {
      return request.getHeaders();
    }
  }


}
Yuval
  • 764
  • 1
  • 9
  • 23
0

love @Yuval's solution!

My original idea was to convert Flux to a local file, and then decompress the local file.

But getting a file downloaded in Spring Reactive is too challenging. I googled a lot, and most of them are blocking way to get file, (e.g. Spring WebClient: How to stream large byte[] to file? and How to correctly read Flux<DataBuffer> and convert it to a single inputStream , none of them works...) which makes no sense and will throw error when calling block() in a reactive flow.

@Yuval saved my day! It works well for me!

shizzhan
  • 41
  • 2