19

How to gzip HTTP request, created by org.springframework.web.client.RestTemplate?

I am using Spring 4.2.6 with Spring Boot 1.3.5 (Java SE, not Android or Javascript in the web browser).

I am making some really big POST requests, and I want request body to be compressed.

Andremoniy
  • 34,031
  • 20
  • 135
  • 241
Bartosz Bilicki
  • 12,599
  • 13
  • 71
  • 113
  • 1
    You can give a look here http://stackoverflow.com/questions/21410317/using-gzip-compression-with-spring-boot-mvc-javaconfig-with-restful there are instructions on how to turn on compression in Boot and non Boot and some performance advices. – Maxvader May 24 '16 at 14:52
  • 1
    @Maxvader , linked answer is for response compression. I want request compression. – Bartosz Bilicki May 24 '16 at 21:34
  • [Compressing HTTP Post Data sent from browser](http://stackoverflow.com/questions/13031968/compressing-http-post-data-sent-from-browser) is about compressing request that orginates from Javascript. My request originates from Java. – Bartosz Bilicki May 25 '16 at 08:19
  • Does you look at http://docs.spring.io/spring-boot/docs/current-SNAPSHOT/reference/htmlsingle/#how-to-enable-http-response-compression ? – iMysak Feb 03 '17 at 07:31
  • 1
    iMysak, liked documentation is about **server response** compression. I would like to compress **request body**. – Bartosz Bilicki Feb 03 '17 at 08:29
  • Any updates on this one? – Ortwin Angermeier Mar 15 '17 at 16:56

3 Answers3

17

I propose two solutions, one simpler without streaming and one that supports streaming.

If you don't require streaming, use a custom ClientHttpRequestInterceptor, a Spring feature.

RestTemplate rt = new RestTemplate();
rt.setInterceptors(Collections.singletonList(interceptor));

Where interceptor could be:

ClientHttpRequestInterceptor interceptor = new ClientHttpRequestInterceptor() {

    @Override
    public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution)
            throws IOException {
        request.getHeaders().add("Content-Encoding", "gzip");
        byte[] gzipped = getGzip(body);
        return execution.execute(request, gzipped);
    } 
 }

getGzip I copied

    private byte[] getGzip(byte[] body) throws IOException {

        ByteArrayOutputStream byteStream = new ByteArrayOutputStream();
        try {
            GZIPOutputStream zipStream = new GZIPOutputStream(byteStream);
            try {
                zipStream.write(body);
            } finally {
                zipStream.close();
            }
        } finally {
            byteStream.close();
        }

        byte[] compressedData = byteStream.toByteArray();
        return compressedData;

    }

After configuring the interceptor all requests will be zipped.

The disadvantage of this approach is that it does not support streaming as the ClientHttpRequestInterceptor receives the content as a byte[]

If you require streaming create a custom ClientHttpRequestFactory, say GZipClientHttpRequestFactory, and use it like this:

    SimpleClientHttpRequestFactory requestFactory = new SimpleClientHttpRequestFactory();
    requestFactory.setBufferRequestBody(false);
    ClientHttpRequestFactory gzipRequestFactory = new GZipClientHttpRequestFactory(requestFactory);
    RestTemplate rt = new RestTemplate(gzipRequestFactory);

Where GZipClientHttpRequestFactory is:

public class GZipClientHttpRequestFactory extends AbstractClientHttpRequestFactoryWrapper {

    public GZipClientHttpRequestFactory(ClientHttpRequestFactory requestFactory) {
        super(requestFactory);
    }

    @Override
    protected ClientHttpRequest createRequest(URI uri, HttpMethod httpMethod, ClientHttpRequestFactory requestFactory)
            throws IOException {
        ClientHttpRequest delegate = requestFactory.createRequest(uri, httpMethod);
        return new ZippedClientHttpRequest(delegate);
    }

}

And ZippedClientHttpRequest is:

public class ZippedClientHttpRequest extends WrapperClientHttpRequest
{
    private GZIPOutputStream zip;

    public ZippedClientHttpRequest(ClientHttpRequest delegate) {
        super(delegate);
        delegate.getHeaders().add("Content-Encoding", "gzip");
        // here or in getBody could add content-length to avoid chunking
        // but is it available ? 
        // delegate.getHeaders().add("Content-Length", "39");

    }

    @Override
    public OutputStream getBody() throws IOException {
        final OutputStream body = super.getBody();
        zip = new GZIPOutputStream(body);
        return zip;
    }

    @Override
    public ClientHttpResponse execute() throws IOException {
        if (zip!=null) zip.close();
        return super.execute();
    }

}

And finally WrapperClientHttpRequest is:

public class WrapperClientHttpRequest implements ClientHttpRequest {

    private final ClientHttpRequest delegate;

    protected WrapperClientHttpRequest(ClientHttpRequest delegate) {
        super();
        if (delegate==null)
            throw new IllegalArgumentException("null delegate");
        this.delegate = delegate;
    }

    protected final ClientHttpRequest getDelegate() {
        return delegate;
    }

    @Override
    public OutputStream getBody() throws IOException {
        return delegate.getBody();
    }

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

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

    @Override
    public HttpMethod getMethod() {
        return delegate.getMethod();
    }

    @Override
    public ClientHttpResponse execute() throws IOException {
        return delegate.execute();
    }
}

This approach creates a request with chunked transfer encoding, this can be changed setting the content length header, if size is known.

The advantage of the ClientHttpRequestInterceptor and/or custom ClientHttpRequestFactory approach is that it works with any method of RestTemplate. An alternate approach, passing a RequestCallback is possible only with execute methods, this because the other methods of RestTemplate internally create their own RequestCallback(s) that produce the content.

BTW it seems that there is little support to decompress gzip request on the server. Also related: Sending gzipped data in WebRequest? that points to the Zip Bomb issue. I think you will have to write some code for it.

Testo Testini
  • 2,200
  • 18
  • 29
  • This interceptor gets full request body and then compresses it in a one step. Am I correct? This is not suitable for sending really big POST requests, as OP requested. – berserkk Jun 09 '17 at 23:21
  • @berserkk Yes, correct, it does not support streaming, I updated the answer. Let's see if is enough or not. The advantage of this is that works with any method of RestTemplate. Passing a RequestCallback is possible only with `execute` methods, for the others is RestTemplate that internally creates its own RequestCallback. btw i am not the downvoter ;-) – Testo Testini Jun 10 '17 at 12:18
  • Your explanation is enough for me. Please, update your answer once again, so I can upvote it later. Thanks. – berserkk Jun 10 '17 at 12:52
  • 1
    I've added an answer to supplement the one above from @TestoTestini to take advantage of Java 7+'s 'try-with-resources' syntax for the `getGzip` method since both OutputStreams are `Closeable`. I couldn't make it look readable in this comment block, hence submitted it as an additional 'Answer' below. – roj Jul 13 '18 at 04:40
2

Further to the above answer from @TestoTestini, if we take advantage of Java 7+'s 'try-with-resources' syntax (since both ByteArrayOutputStream and GZIPOutputStream implement closeable() ) then we can shrink the getGzip function into the following:

private byte[] getGzip(byte[] body) throws IOException {

    try (ByteArrayOutputStream byteStream = new ByteArrayOutputStream();
         GZIPOutputStream zipStream = new GZIPOutputStream(byteStream)) {
            zipStream.write(body);
        byte[] compressedData = byteStream.toByteArray();
        return compressedData;
    }

}

(I couldn't find a way of commenting on @TestoTestini's original answer and retaining the above code format, hence this Answer).

roj
  • 1,262
  • 13
  • 27
0

Since I cannot comment on @roj 's post I'm writing an answer here.

@roj snippet although is neat it actually does not do the same job as @Testo Testini 's snippet.

Testo is closing the streams before: byteStream.toByteArray(); where in @rog answer, this occurs before the stream.close(), since streams are in the try/resource block.

If you need to use try-with-resources, zipStream should be closed before the byteStream.toByteArray();

The complete snippet should be:

private byte[] getGzip(byte[] body) throws IOException {

    try (ByteArrayOutputStream byteStream = new ByteArrayOutputStream();
         GZIPOutputStream zipStream = new GZIPOutputStream(byteStream)) {
            
        zipStream.write(body);
        zipStream.close();

        byte[] compressedData = byteStream.toByteArray();
        return compressedData;
    }

}

The was getting an error ("Compressed file ended before the end-of-stream marker was reached") and the above fixed the error in my case and I thought that I should share this.

alobar
  • 71
  • 1
  • 7