diff --git a/docs/zipper.jpg b/docs/zipper.jpg new file mode 100644 index 000000000..e83bfa447 Binary files /dev/null and b/docs/zipper.jpg differ diff --git a/pom.xml b/pom.xml index fed4992d9..cb4cbae02 100644 --- a/pom.xml +++ b/pom.xml @@ -54,6 +54,7 @@ riptide-compatibility riptide-core riptide-chaos + riptide-compression riptide-failsafe riptide-faults riptide-httpclient @@ -104,6 +105,11 @@ riptide-compatibility ${project.version} + + org.zalando + riptide-compression + ${project.version} + org.zalando riptide-core @@ -607,7 +613,6 @@ -Xlint:unchecked - -Werror -parameters diff --git a/riptide-compression/README.md b/riptide-compression/README.md new file mode 100644 index 000000000..c94bd81a5 --- /dev/null +++ b/riptide-compression/README.md @@ -0,0 +1,81 @@ +# Riptide: Compression + +[![Zipper](../docs/zipper.jpg)](https://pixabay.com/photos/zipper-metal-gold-color-brass-201684/) + +[![Build Status](https://img.shields.io/travis/zalando/riptide.svg)](https://travis-ci.org/zalando/riptide) +[![Coverage Status](https://img.shields.io/coveralls/zalando/riptide.svg)](https://coveralls.io/r/zalando/riptide) +[![Code Quality](https://img.shields.io/codacy/grade/1fbe3d16ca544c0c8589692632d114de/master.svg)](https://www.codacy.com/app/whiskeysierra/riptide) +[![Javadoc](https://www.javadoc.io/badge/org.zalando/riptide-compression.svg)](http://www.javadoc.io/doc/org.zalando/riptide-compression) +[![Release](https://img.shields.io/github/release/zalando/riptide.svg)](https://github.com/zalando/riptide/releases) +[![Maven Central](https://img.shields.io/maven-central/v/org.zalando/riptide-compression.svg)](https://maven-badges.herokuapp.com/maven-central/org.zalando/riptide-compression) +[![License](https://img.shields.io/badge/license-MIT-blue.svg)](https://raw.githubusercontent.com/zalando/riptide/master/LICENSE) + +*Riptide: Compression* adds support to compress request bodies. + +## Features + +- pluggable compression mechanism +- out of the box GZIP support + +## Dependencies + +- Java 8 +- Riptide: Core + +## Installation + +Add the following dependency to your project: + +```xml + + org.zalando + riptide-compression + ${riptide.version} + +``` + +## Configuration + +```java +Http.builder() + .plugin(new RequestCompressionPlugin()) + .build(); +``` + +By default request bodies are compressed using [GZIP](https://docs.oracle.com/javase/8/docs/api/java/util/zip/GZIPOutputStream.html). + +In order to specify the compression algorithm you can pass in a custom `Compression`: + +```java +new RequestCompressionPlugin(Compression.of("br", BrotliOutputStream::new)); +``` + +## Usage + +```java +http.post("/events") + .contentType(MediaType.APPLICATION_JSON) + .body(asList(events)) + .call(pass()) + .join(); +``` + +All request bodies will be compressed using the configured compression method using `chunked` transfer-encoding. + +If there is already a `Content-Encoding` specified on the request, the plugin does nothing. + +### Limitations + +* You must only configure a single `RequestCompressionPlugin` as only a single encoding is applied currently. +* Starting with Spring 4.3 the `Netty4ClientHttpRequestFactory` unconditionally adds a `Content-Length` header, +which breaks if used together with `RequestCompressionPlugin`. Use `riptide-httpclient` instead. + + +## Getting Help + +If you have questions, concerns, bug reports, etc., please file an issue in this repository's [Issue Tracker](../../../../issues). + +## Getting Involved/Contributing + +To contribute, simply make a pull request and add a brief description (1-2 sentences) of your addition or change. For +more details, check the [contribution guidelines](../.github/CONTRIBUTING.md). diff --git a/riptide-compression/pom.xml b/riptide-compression/pom.xml new file mode 100644 index 000000000..a71fecbfa --- /dev/null +++ b/riptide-compression/pom.xml @@ -0,0 +1,44 @@ + + + 4.0.0 + + + org.zalando + riptide-parent + 3.0.0-SNAPSHOT + + + riptide-compression + + Riptide: Request Compression + Client side response routing with stream support + + + + org.zalando + riptide-core + + + org.zalando + riptide-httpclient + test + + + io.netty + netty-all + 4.1.43.Final + test + + + org.springframework + spring-test + + + com.github.rest-driver + rest-client-driver + + + + diff --git a/riptide-compression/src/main/java/org/zalando/riptide/compression/Compression.java b/riptide-compression/src/main/java/org/zalando/riptide/compression/Compression.java new file mode 100644 index 000000000..01fda3ade --- /dev/null +++ b/riptide-compression/src/main/java/org/zalando/riptide/compression/Compression.java @@ -0,0 +1,19 @@ +package org.zalando.riptide.compression; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import org.apiguardian.api.API; +import org.zalando.fauxpas.ThrowingUnaryOperator; + +import java.io.IOException; +import java.io.OutputStream; + +import static org.apiguardian.api.API.Status.EXPERIMENTAL; + +@API(status = EXPERIMENTAL) +@AllArgsConstructor(staticName = "of") +@Getter +public final class Compression { + private final String contentEncoding; + private final ThrowingUnaryOperator outputStreamDecorator; +} diff --git a/riptide-compression/src/main/java/org/zalando/riptide/compression/RequestCompressionPlugin.java b/riptide-compression/src/main/java/org/zalando/riptide/compression/RequestCompressionPlugin.java new file mode 100644 index 000000000..31b50bc2a --- /dev/null +++ b/riptide-compression/src/main/java/org/zalando/riptide/compression/RequestCompressionPlugin.java @@ -0,0 +1,88 @@ +package org.zalando.riptide.compression; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import org.apiguardian.api.API; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpOutputMessage; +import org.springframework.http.StreamingHttpOutputMessage; +import org.zalando.riptide.Plugin; +import org.zalando.riptide.RequestArguments.Entity; +import org.zalando.riptide.RequestExecution; + +import java.io.IOException; +import java.io.OutputStream; +import java.util.zip.GZIPOutputStream; + +import static org.apiguardian.api.API.Status.EXPERIMENTAL; +import static org.springframework.http.HttpHeaders.CONTENT_ENCODING; +import static org.springframework.http.HttpHeaders.TRANSFER_ENCODING; + +@API(status = EXPERIMENTAL) +public final class RequestCompressionPlugin implements Plugin { + + private final Compression compression; + + public RequestCompressionPlugin(final Compression compression) { + this.compression = compression; + } + + public RequestCompressionPlugin() { + this(Compression.of("gzip", GZIPOutputStream::new)); + } + + @Override + public RequestExecution aroundNetwork(final RequestExecution execution) { + return arguments -> { + final Entity entity = arguments.getEntity(); + + if (entity.isEmpty() || arguments.getHeaders().containsKey(CONTENT_ENCODING)) { + return execution.execute(arguments); + } + + return execution.execute( + arguments.withEntity(new CompressingEntity(compression, entity))); + }; + } + + @AllArgsConstructor + private static class CompressingEntity implements Entity { + + private final Compression compression; + private final Entity entity; + + @Override + public void writeTo(final HttpOutputMessage message) throws IOException { + update(message.getHeaders()); + + if (message instanceof StreamingHttpOutputMessage) { + final StreamingHttpOutputMessage streaming = (StreamingHttpOutputMessage) message; + streaming.setBody(stream -> + writeToCompressing(new DelegatingHttpOutputMessage(message.getHeaders(), stream))); + } else { + writeToCompressing(message); + } + } + + private void writeToCompressing(HttpOutputMessage message) throws IOException { + try (final WrappingHttpOutputMessage compressingMessage = + new WrappingHttpOutputMessage(message, compression.getOutputStreamDecorator())) { + entity.writeTo(compressingMessage); + } + } + + private void update(final HttpHeaders headers) { + headers.set(CONTENT_ENCODING, compression.getContentEncoding()); + headers.set(TRANSFER_ENCODING, "chunked"); + } + + } + + @AllArgsConstructor + @Getter + private static final class DelegatingHttpOutputMessage implements HttpOutputMessage { + private final HttpHeaders headers; + private final OutputStream body; + } + +} diff --git a/riptide-compression/src/main/java/org/zalando/riptide/compression/WrappingHttpOutputMessage.java b/riptide-compression/src/main/java/org/zalando/riptide/compression/WrappingHttpOutputMessage.java new file mode 100644 index 000000000..c3295e23b --- /dev/null +++ b/riptide-compression/src/main/java/org/zalando/riptide/compression/WrappingHttpOutputMessage.java @@ -0,0 +1,44 @@ +package org.zalando.riptide.compression; + +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpOutputMessage; +import org.zalando.fauxpas.ThrowingUnaryOperator; + +import javax.annotation.Nonnull; +import java.io.IOException; +import java.io.OutputStream; + +final class WrappingHttpOutputMessage implements HttpOutputMessage, AutoCloseable { + + private final HttpOutputMessage message; + private final ThrowingUnaryOperator wrapper; + private OutputStream stream; + + WrappingHttpOutputMessage(HttpOutputMessage message, ThrowingUnaryOperator wrapper) { + this.message = message; + this.wrapper = wrapper; + } + + @Nonnull + @Override + public OutputStream getBody() throws IOException { + if (stream == null) { + stream = wrapper.apply(message.getBody()); + } + return stream; + } + + @Nonnull + @Override + public HttpHeaders getHeaders() { + return message.getHeaders(); + } + + @Override + public void close() throws IOException { + // make sure any underlying compressor gets flushed + if (stream != null) { + stream.close(); + } + } +} diff --git a/riptide-compression/src/main/java/org/zalando/riptide/compression/package-info.java b/riptide-compression/src/main/java/org/zalando/riptide/compression/package-info.java new file mode 100644 index 000000000..7870a37f8 --- /dev/null +++ b/riptide-compression/src/main/java/org/zalando/riptide/compression/package-info.java @@ -0,0 +1,4 @@ +@ParametersAreNonnullByDefault +package org.zalando.riptide.compression; + +import javax.annotation.ParametersAreNonnullByDefault; \ No newline at end of file diff --git a/riptide-compression/src/test/java/org/zalando/riptide/compression/GzipClientDriver.java b/riptide-compression/src/test/java/org/zalando/riptide/compression/GzipClientDriver.java new file mode 100644 index 000000000..0d19cfa87 --- /dev/null +++ b/riptide-compression/src/test/java/org/zalando/riptide/compression/GzipClientDriver.java @@ -0,0 +1,58 @@ +package org.zalando.riptide.compression; + +import com.github.restdriver.clientdriver.ClientDriver; +import com.github.restdriver.clientdriver.DefaultRequestMatcher; +import com.github.restdriver.clientdriver.exception.ClientDriverSetupException; +import com.github.restdriver.clientdriver.jetty.ClientDriverJettyHandler; +import com.github.restdriver.clientdriver.jetty.DefaultClientDriverJettyHandler; +import com.google.gag.annotation.remark.Hack; +import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.server.ServerConnector; +import org.eclipse.jetty.server.handler.gzip.GzipHandler; + +@Hack +final class GzipClientDriver extends ClientDriver { + + private int port; + + static ClientDriver create() { + return new GzipClientDriver(new DefaultClientDriverJettyHandler(new DefaultRequestMatcher())); + } + + private GzipClientDriver(ClientDriverJettyHandler handler) { + super(handler); + } + + @Override + protected Server createAndStartJetty(int port) { + final Server jetty = new Server(); + jetty.setHandler(handler); + final GzipHandler gzip = new GzipHandler(); + gzip.setInflateBufferSize(1024); + jetty.insertHandler(gzip); + final ServerConnector connector = createConnector(jetty, port); + jetty.addConnector(connector); + try { + jetty.start(); + } catch (Exception e) { + throw new ClientDriverSetupException("Error starting jetty on port " + port, e); + } + this.port = connector.getLocalPort(); + return jetty; + } + + @Override + public int getPort() { + return port; + } + + @Override + public String getBaseUrl() { + return "http://localhost:" + port; + } + + @Override + protected void replaceConnector(ServerConnector newConnector, Server jetty) { + throw new UnsupportedOperationException(); + } +} diff --git a/riptide-compression/src/test/java/org/zalando/riptide/compression/RequestCompressionPluginTest.java b/riptide-compression/src/test/java/org/zalando/riptide/compression/RequestCompressionPluginTest.java new file mode 100644 index 000000000..d91ac2e29 --- /dev/null +++ b/riptide-compression/src/test/java/org/zalando/riptide/compression/RequestCompressionPluginTest.java @@ -0,0 +1,140 @@ +package org.zalando.riptide.compression; + +import com.github.restdriver.clientdriver.ClientDriver; +import org.apache.http.impl.client.HttpClients; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.ArgumentsProvider; +import org.junit.jupiter.params.provider.ArgumentsSource; +import org.springframework.http.MediaType; +import org.springframework.http.client.BufferingClientHttpRequestFactory; +import org.springframework.http.client.ClientHttpRequestFactory; +import org.springframework.http.client.SimpleClientHttpRequestFactory; +import org.zalando.riptide.Http; +import org.zalando.riptide.Plugin; +import org.zalando.riptide.httpclient.ApacheClientHttpRequestFactory; +import org.zalando.riptide.httpclient.ApacheClientHttpRequestFactory.Mode; + +import java.util.HashMap; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.TimeUnit; +import java.util.stream.Stream; + +import static com.github.restdriver.clientdriver.ClientDriverRequest.Method.POST; +import static com.github.restdriver.clientdriver.RestClientDriver.giveResponse; +import static com.github.restdriver.clientdriver.RestClientDriver.onRequestTo; +import static java.util.Arrays.asList; +import static java.util.concurrent.Executors.newSingleThreadExecutor; +import static org.hamcrest.Matchers.emptyString; +import static org.hamcrest.Matchers.equalTo; +import static org.springframework.http.HttpHeaders.CONTENT_ENCODING; +import static org.zalando.riptide.PassRoute.pass; + +class RequestCompressionPluginTest { + + private final ClientDriver driver = GzipClientDriver.create(); + private final ExecutorService executor = newSingleThreadExecutor(); + + @AfterEach + void tearDown() throws Exception { + executor.shutdown(); + executor.awaitTermination(10, TimeUnit.SECONDS); + driver.verify(); + } + + @ParameterizedTest + @ArgumentsSource(RequestFactorySource.class) + void shouldCompressRequestBody(final ClientHttpRequestFactory factory) { + driver.addExpectation(onRequestTo("/") + .withMethod(POST) + .withHeader("X-Content-Encoding", "gzip") // written by Jetty's GzipHandler + .withBody(equalTo("{}"), "application/json"), + giveResponse("", "text/plain")); + + final Http http = buildHttp(factory, new RequestCompressionPlugin()); + http.post("/") + .contentType(MediaType.APPLICATION_JSON) + .body(new HashMap<>()) + .call(pass()) + .join(); + } + + @ParameterizedTest + @ArgumentsSource(RequestFactorySource.class) + void shouldNotCompressEmptyRequestBody(final ClientHttpRequestFactory factory) { + driver.addExpectation(onRequestTo("/") + .withMethod(POST) + .withBody(emptyString(), "application/json") + .withoutHeader("Content-Encoding") + .withoutHeader("X-Content-Encoding"), + giveResponse("", "text/plain")); + + final Http http = buildHttp(factory, new RequestCompressionPlugin()); + http.post("/") + .contentType(MediaType.APPLICATION_JSON) + .call(pass()) + .join(); + } + + @ParameterizedTest + @ArgumentsSource(RequestFactorySource.class) + void shouldCompressWithGivenAlgorithm(final ClientHttpRequestFactory factory) { + driver.addExpectation(onRequestTo("/") + .withMethod(POST) + .withHeader("Content-Encoding", "identity") // not handled by Jetty + .withoutHeader("X-Content-Encoding") + .withBody(equalTo("{}"), "application/json"), + giveResponse("", "text/plain")); + + final Http http = buildHttp(factory, new RequestCompressionPlugin(Compression.of("identity", it -> it))); + http.post("/") + .contentType(MediaType.APPLICATION_JSON) + .body(new HashMap<>()) + .call(pass()) + .join(); + } + + @ParameterizedTest + @ArgumentsSource(RequestFactorySource.class) + void shouldBackOffIfAlreadyEncoded(final ClientHttpRequestFactory factory) { + driver.addExpectation(onRequestTo("/") + .withMethod(POST) + .withHeader("Content-Encoding", "custom") // not handled by Jetty + .withoutHeader("X-Content-Encoding") + .withBody(equalTo("{}"), "application/json"), + giveResponse("", "text/plain")); + + final Http http = buildHttp(factory, new RequestCompressionPlugin()); + http.post("/") + .header(CONTENT_ENCODING, "custom") + .contentType(MediaType.APPLICATION_JSON) + .body(new HashMap<>()) + .call(pass()) + .join(); + } + + private Http buildHttp(final ClientHttpRequestFactory factory, final Plugin... plugins) { + return Http.builder() + .executor(executor) + .requestFactory(factory) + .baseUrl(driver.getBaseUrl()) + .plugins(asList(plugins)) + .build(); + } + + static class RequestFactorySource implements ArgumentsProvider { + @Override + public Stream provideArguments(ExtensionContext extensionContext) { + return Stream.of( + new SimpleClientHttpRequestFactory(), + // new Netty4ClientHttpRequestFactory(), # broken, see #823 + new BufferingClientHttpRequestFactory(new SimpleClientHttpRequestFactory()), + new ApacheClientHttpRequestFactory(HttpClients.createDefault(), Mode.BUFFERING), + new ApacheClientHttpRequestFactory(HttpClients.createDefault(), Mode.STREAMING) + ).map(Arguments::of); + } + } + +} diff --git a/riptide-compression/src/test/java/org/zalando/riptide/compression/WrappingHttpOutputMessageTest.java b/riptide-compression/src/test/java/org/zalando/riptide/compression/WrappingHttpOutputMessageTest.java new file mode 100644 index 000000000..4c42e6d98 --- /dev/null +++ b/riptide-compression/src/test/java/org/zalando/riptide/compression/WrappingHttpOutputMessageTest.java @@ -0,0 +1,72 @@ +package org.zalando.riptide.compression; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpOutputMessage; +import org.zalando.fauxpas.ThrowingUnaryOperator; + +import java.io.IOException; +import java.io.OutputStream; + +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; +import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.mockito.Mockito.when; + +class WrappingHttpOutputMessageTest { + + private final HttpOutputMessage message = mock(HttpOutputMessage.class); + + @SuppressWarnings("unchecked") + private final ThrowingUnaryOperator wrapper = mock(ThrowingUnaryOperator.class); + + private final WrappingHttpOutputMessage unit = new WrappingHttpOutputMessage(message, wrapper); + + @BeforeEach + void setUp() throws IOException { + final OutputStream body = mock(OutputStream.class); + when(message.getBody()).thenReturn(body); + when(message.getHeaders()).thenReturn(new HttpHeaders()); + } + + @Test + void shouldDelegateHeaders() { + assertThat(unit.getHeaders(), equalTo(message.getHeaders())); + } + + @Test + void shouldReturnWrapped() throws IOException { + final OutputStream wrappedStream = mock(OutputStream.class); + when(wrapper.apply(any())).thenReturn(wrappedStream); + + assertThat(unit.getBody(), equalTo(wrappedStream)); + assertThat(unit.getBody(), equalTo(wrappedStream)); + } + + @Test + void shouldWrapOnFirstAccessOnly() throws IOException { + final OutputStream wrappedStream = mock(OutputStream.class); + when(wrapper.apply(any())).thenReturn(wrappedStream); + + unit.getBody(); + unit.getBody(); + + verify(wrapper).apply(message.getBody()); + verifyNoMoreInteractions(wrapper); + } + + @Test + void shouldNotCloseStreamIfNeverAccessed() throws IOException { + final OutputStream wrappedStream = mock(OutputStream.class); + when(wrapper.apply(any())).thenReturn(wrappedStream); + + unit.close(); + + verifyNoInteractions(wrappedStream); + } +} diff --git a/riptide-core/src/main/java/org/zalando/riptide/RequestCompressionPlugin.java b/riptide-core/src/main/java/org/zalando/riptide/RequestCompressionPlugin.java deleted file mode 100644 index fdc6026fb..000000000 --- a/riptide-core/src/main/java/org/zalando/riptide/RequestCompressionPlugin.java +++ /dev/null @@ -1,70 +0,0 @@ -package org.zalando.riptide; - -import lombok.AllArgsConstructor; -import org.apiguardian.api.API; -import org.springframework.http.HttpHeaders; -import org.springframework.http.HttpOutputMessage; -import org.zalando.riptide.RequestArguments.Entity; - -import javax.annotation.Nonnull; -import java.io.IOException; -import java.io.OutputStream; -import java.util.zip.GZIPOutputStream; - -import static org.apiguardian.api.API.Status.EXPERIMENTAL; - -@API(status = EXPERIMENTAL) -public final class RequestCompressionPlugin implements Plugin { - - @Override - public RequestExecution aroundNetwork(final RequestExecution execution) { - return arguments -> { - final Entity entity = arguments.getEntity(); - - if (entity.isEmpty()) { - return execution.execute(arguments); - } - - return execution.execute( - arguments.withEntity(new GzipEntity(entity))); - }; - } - - @AllArgsConstructor - private static class GzipEntity implements Entity { - - private final Entity entity; - - @Override - public void writeTo(final HttpOutputMessage message) throws IOException { - entity.writeTo(new GzipHttpOutputMessage(message)); - update(message.getHeaders()); - } - - private void update(final HttpHeaders headers) { - headers.set("Content-Encoding", "gzip"); - headers.set("Transfer-Encoding", "chunked"); - } - - } - - @AllArgsConstructor - private static final class GzipHttpOutputMessage implements HttpOutputMessage { - - private final HttpOutputMessage message; - - @Nonnull - @Override - public OutputStream getBody() throws IOException { - return new GZIPOutputStream(message.getBody()); - } - - @Nonnull - @Override - public HttpHeaders getHeaders() { - return message.getHeaders(); - } - - } - -} diff --git a/riptide-core/src/test/java/org/zalando/riptide/RequestCompressionPluginTest.java b/riptide-core/src/test/java/org/zalando/riptide/RequestCompressionPluginTest.java deleted file mode 100644 index f06330433..000000000 --- a/riptide-core/src/test/java/org/zalando/riptide/RequestCompressionPluginTest.java +++ /dev/null @@ -1,69 +0,0 @@ -package org.zalando.riptide; - -import com.github.restdriver.clientdriver.ClientDriver; -import com.github.restdriver.clientdriver.ClientDriverFactory; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.Test; -import org.springframework.http.MediaType; -import org.springframework.http.client.SimpleClientHttpRequestFactory; - -import java.util.HashMap; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.TimeUnit; - -import static com.github.restdriver.clientdriver.ClientDriverRequest.Method.POST; -import static com.github.restdriver.clientdriver.RestClientDriver.giveResponse; -import static com.github.restdriver.clientdriver.RestClientDriver.onRequestTo; -import static java.util.concurrent.Executors.newSingleThreadExecutor; -import static org.hamcrest.Matchers.emptyString; -import static org.hamcrest.Matchers.equalTo; -import static org.hamcrest.Matchers.not; -import static org.zalando.riptide.PassRoute.pass; - -final class RequestCompressionPluginTest { - - private final ClientDriver driver = new ClientDriverFactory().createClientDriver(); - private final ExecutorService executor = newSingleThreadExecutor(); - - private final Http unit = Http.builder() - .executor(executor) - .requestFactory(new SimpleClientHttpRequestFactory()) - .baseUrl(driver.getBaseUrl()) - .plugin(new RequestCompressionPlugin()) - .build(); - - @AfterEach - void tearDown() throws Exception { - executor.shutdown(); - executor.awaitTermination(10, TimeUnit.SECONDS); - driver.verify(); - } - - @Test - void shouldNotCompressEmptyBody() { - driver.addExpectation(onRequestTo("/") - .withBody(emptyString(), "text/plain") - .withoutHeader("Content-Encoding"), - giveResponse("", "text/plain")); - - unit.get("/") - .contentType(MediaType.TEXT_PLAIN) - .call(pass()) - .join(); - } - - @Test - void shouldCompressNonEmptyBody() { - driver.addExpectation(onRequestTo("/") - .withMethod(POST) - .withBody(not(equalTo("{}")), "application/json") - .withHeader("Content-Encoding", "gzip"), - giveResponse("", "text/plain")); - - unit.post("/") - .body(new HashMap<>()) - .call(pass()) - .join(); - } - -} diff --git a/riptide-logbook/pom.xml b/riptide-logbook/pom.xml index 6644538e1..b9cfd1191 100644 --- a/riptide-logbook/pom.xml +++ b/riptide-logbook/pom.xml @@ -42,6 +42,11 @@ riptide-httpclient test + + org.zalando + riptide-compression + test + org.zalando logbook-json diff --git a/riptide-logbook/src/test/java/org/zalando/riptide/logbook/LogbookPluginTest.java b/riptide-logbook/src/test/java/org/zalando/riptide/logbook/LogbookPluginTest.java index 8c8d3169f..d24144961 100644 --- a/riptide-logbook/src/test/java/org/zalando/riptide/logbook/LogbookPluginTest.java +++ b/riptide-logbook/src/test/java/org/zalando/riptide/logbook/LogbookPluginTest.java @@ -16,7 +16,7 @@ import org.zalando.logbook.Precorrelation; import org.zalando.logbook.json.JsonHttpLogFormatter; import org.zalando.riptide.Http; -import org.zalando.riptide.RequestCompressionPlugin; +import org.zalando.riptide.compression.RequestCompressionPlugin; import java.io.IOException; import java.util.concurrent.CompletableFuture; diff --git a/riptide-spring-boot-autoconfigure/README.md b/riptide-spring-boot-autoconfigure/README.md index 8af18efe0..1c8036d71 100644 --- a/riptide-spring-boot-autoconfigure/README.md +++ b/riptide-spring-boot-autoconfigure/README.md @@ -80,6 +80,7 @@ private Http example; - Riptide - Core - (Apache) HTTP Client + - Compression - Backup (optional) - Failsafe (optional) - Faults (optional) diff --git a/riptide-spring-boot-autoconfigure/pom.xml b/riptide-spring-boot-autoconfigure/pom.xml index 6e6f53281..80cd54524 100644 --- a/riptide-spring-boot-autoconfigure/pom.xml +++ b/riptide-spring-boot-autoconfigure/pom.xml @@ -65,6 +65,10 @@ org.zalando riptide-compatibility + + org.zalando + riptide-compression + org.zalando riptide-core diff --git a/riptide-spring-boot-autoconfigure/src/main/java/org/zalando/riptide/autoconfigure/DefaultRiptideRegistrar.java b/riptide-spring-boot-autoconfigure/src/main/java/org/zalando/riptide/autoconfigure/DefaultRiptideRegistrar.java index 7b25a5a0a..515a7ba2d 100644 --- a/riptide-spring-boot-autoconfigure/src/main/java/org/zalando/riptide/autoconfigure/DefaultRiptideRegistrar.java +++ b/riptide-spring-boot-autoconfigure/src/main/java/org/zalando/riptide/autoconfigure/DefaultRiptideRegistrar.java @@ -27,7 +27,6 @@ import org.zalando.riptide.Http; import org.zalando.riptide.OriginalStackTracePlugin; import org.zalando.riptide.Plugin; -import org.zalando.riptide.RequestCompressionPlugin; import org.zalando.riptide.auth.AuthorizationPlugin; import org.zalando.riptide.auth.AuthorizationProvider; import org.zalando.riptide.auth.PlatformCredentialsAuthorizationProvider; @@ -41,6 +40,7 @@ import org.zalando.riptide.chaos.Probability; import org.zalando.riptide.compatibility.AsyncHttpOperations; import org.zalando.riptide.compatibility.HttpOperations; +import org.zalando.riptide.compression.RequestCompressionPlugin; import org.zalando.riptide.failsafe.BackupRequest; import org.zalando.riptide.failsafe.CircuitBreakerListener; import org.zalando.riptide.failsafe.FailsafePlugin; diff --git a/riptide-spring-boot-autoconfigure/src/test/java/org/zalando/riptide/autoconfigure/ManualConfiguration.java b/riptide-spring-boot-autoconfigure/src/test/java/org/zalando/riptide/autoconfigure/ManualConfiguration.java index 4da0c4cd2..11ccc6669 100644 --- a/riptide-spring-boot-autoconfigure/src/test/java/org/zalando/riptide/autoconfigure/ManualConfiguration.java +++ b/riptide-spring-boot-autoconfigure/src/test/java/org/zalando/riptide/autoconfigure/ManualConfiguration.java @@ -32,7 +32,6 @@ import org.zalando.riptide.Http; import org.zalando.riptide.OriginalStackTracePlugin; import org.zalando.riptide.Plugin; -import org.zalando.riptide.RequestCompressionPlugin; import org.zalando.riptide.UrlResolution; import org.zalando.riptide.auth.AuthorizationPlugin; import org.zalando.riptide.auth.PlatformCredentialsAuthorizationProvider; @@ -44,6 +43,7 @@ import org.zalando.riptide.chaos.Probability; import org.zalando.riptide.compatibility.AsyncHttpOperations; import org.zalando.riptide.compatibility.HttpOperations; +import org.zalando.riptide.compression.RequestCompressionPlugin; import org.zalando.riptide.failsafe.BackupRequest; import org.zalando.riptide.failsafe.CircuitBreakerListener; import org.zalando.riptide.failsafe.CompositeDelayFunction;