-
Notifications
You must be signed in to change notification settings - Fork 20
Add support for request compression #968
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 2 commits
28be2c7
ee7afed
117140b
a9b57b7
f5e56bc
69edbc9
508bd0b
9eb4896
8f15589
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,31 @@ | ||
| /* | ||
| * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. | ||
| * SPDX-License-Identifier: Apache-2.0 | ||
| */ | ||
|
|
||
| package software.amazon.smithy.java.client.http.compression; | ||
|
|
||
| import java.util.List; | ||
| import software.amazon.smithy.java.io.datastream.DataStream; | ||
| import software.amazon.smithy.utils.ListUtils; | ||
joewyz marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
|
||
| /** | ||
| * Represents a compression algorithm that can be used to compress request | ||
| * bodies. | ||
| */ | ||
| public interface CompressionAlgorithm { | ||
joewyz marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| /** | ||
| * The ID of the compression algorithm. This is matched against the algorithm | ||
| * names used in the trait e.g. "gzip" | ||
| */ | ||
| String algorithmId(); | ||
|
|
||
| /** | ||
| * Compresses content of fixed length | ||
| */ | ||
| DataStream compress(DataStream data); | ||
|
|
||
| static List<CompressionAlgorithm> supportedAlgorithms() { | ||
| return ListUtils.of(new Gzip()); | ||
| } | ||
joewyz marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,40 @@ | ||
| /* | ||
| * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. | ||
| * SPDX-License-Identifier: Apache-2.0 | ||
| */ | ||
|
|
||
| package software.amazon.smithy.java.client.http.compression; | ||
|
|
||
| import java.io.IOException; | ||
| import java.io.UncheckedIOException; | ||
| import java.util.zip.GZIPOutputStream; | ||
| import software.amazon.smithy.java.io.ByteBufferOutputStream; | ||
| import software.amazon.smithy.java.io.datastream.DataStream; | ||
|
|
||
| public final class Gzip implements CompressionAlgorithm { | ||
|
|
||
| @Override | ||
| public String algorithmId() { | ||
| return "gzip"; | ||
| } | ||
|
|
||
| @Override | ||
| public DataStream compress(DataStream data) { | ||
| if (!data.hasKnownLength()) { // Using streaming | ||
joewyz marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| return DataStream.ofInputStream( | ||
| new GzipCompressingInputStream(data.asInputStream()), | ||
| data.contentType(), | ||
| -1); | ||
| } | ||
|
|
||
| try (var bos = new ByteBufferOutputStream(); | ||
| var in = data.asInputStream()) { | ||
| var gzip = new GZIPOutputStream(bos); | ||
| in.transferTo(gzip); | ||
| gzip.close(); | ||
joewyz marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| return DataStream.ofBytes(bos.toByteBuffer().array()); | ||
| } catch (IOException e) { | ||
| throw new UncheckedIOException(e); | ||
| } | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,56 @@ | ||
| /* | ||
| * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. | ||
| * SPDX-License-Identifier: Apache-2.0 | ||
| */ | ||
|
|
||
| package software.amazon.smithy.java.client.http.compression; | ||
|
|
||
| import java.io.ByteArrayOutputStream; | ||
| import java.io.IOException; | ||
| import java.io.InputStream; | ||
| import java.util.zip.GZIPOutputStream; | ||
|
|
||
| final class GzipCompressingInputStream extends InputStream { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Add javadoc.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This implementation underneath still reads the whole input stream into memory to do the compression. |
||
|
|
||
| private final InputStream source; | ||
| private byte[] compressedData; | ||
| private int position = 0; | ||
|
|
||
| public GzipCompressingInputStream(InputStream source) { | ||
| this.source = source; | ||
| } | ||
|
|
||
| @Override | ||
| public int read() throws IOException { | ||
| ensureCompressed(); | ||
| return position < compressedData.length ? compressedData[position++] & 0xFF : -1; | ||
| } | ||
|
|
||
| @Override | ||
| public int read(byte[] b, int off, int len) throws IOException { | ||
| ensureCompressed(); | ||
| var available = compressedData.length - position; | ||
| if (available <= 0) | ||
| return -1; | ||
|
||
|
|
||
| var toRead = Math.min(len, available); | ||
| System.arraycopy(compressedData, position, b, off, toRead); | ||
| position += toRead; | ||
| return toRead; | ||
| } | ||
|
|
||
| private void ensureCompressed() throws IOException { | ||
| if (compressedData == null) { | ||
| var buffer = new ByteArrayOutputStream(); | ||
| try (GZIPOutputStream gzip = new GZIPOutputStream(buffer)) { | ||
| source.transferTo(gzip); | ||
| } | ||
|
||
| compressedData = buffer.toByteArray(); | ||
| } | ||
| } | ||
|
|
||
| @Override | ||
| public void close() throws IOException { | ||
| source.close(); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,103 @@ | ||
| /* | ||
| * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. | ||
| * SPDX-License-Identifier: Apache-2.0 | ||
| */ | ||
|
|
||
| package software.amazon.smithy.java.client.http.plugins; | ||
|
|
||
| import java.util.List; | ||
| import software.amazon.smithy.java.client.core.AutoClientPlugin; | ||
| import software.amazon.smithy.java.client.core.ClientConfig; | ||
| import software.amazon.smithy.java.client.core.interceptors.ClientInterceptor; | ||
| import software.amazon.smithy.java.client.core.interceptors.RequestHook; | ||
| import software.amazon.smithy.java.client.http.HttpContext; | ||
| import software.amazon.smithy.java.client.http.HttpMessageExchange; | ||
| import software.amazon.smithy.java.client.http.compression.CompressionAlgorithm; | ||
| import software.amazon.smithy.java.context.Context; | ||
| import software.amazon.smithy.java.core.schema.TraitKey; | ||
| import software.amazon.smithy.java.http.api.HttpRequest; | ||
| import software.amazon.smithy.java.io.datastream.DataStream; | ||
| import software.amazon.smithy.model.traits.RequestCompressionTrait; | ||
| import software.amazon.smithy.utils.SmithyInternalApi; | ||
|
|
||
| /** | ||
| * Compress the request body using provided compression algorithm if @requestCompression trait is applied. | ||
| */ | ||
| @SmithyInternalApi | ||
| public final class RequestCompressionPlugin implements AutoClientPlugin { | ||
|
|
||
| @Override | ||
| public void configureClient(ClientConfig.Builder config) { | ||
| if (config.isUsingMessageExchange(HttpMessageExchange.INSTANCE)) { | ||
| config.addInterceptor(RequestCompressionInterceptor.INSTANCE); | ||
| } | ||
| } | ||
|
|
||
| static final class RequestCompressionInterceptor implements ClientInterceptor { | ||
|
|
||
| private static final int DEFAULT_MIN_COMPRESSION_SIZE_BYTES = 10240; | ||
| // This cap matches ApiGateway's spec: https://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-openapi-minimum-compression-size.html | ||
| private static final int MIN_COMPRESSION_SIZE_CAP = 10485760; | ||
| private static final String CONTENT_ENCODING_HEADER = "Content-Encoding"; | ||
| private static final ClientInterceptor INSTANCE = new RequestCompressionInterceptor(); | ||
| private static final TraitKey<RequestCompressionTrait> REQUEST_COMPRESSION_TRAIT_KEY = | ||
| TraitKey.get(RequestCompressionTrait.class); | ||
| // Currently only Gzip is supported in Smithy model: https://smithy.io/2.0/spec/behavior-traits.html#requestcompression-trait | ||
| private static final List<CompressionAlgorithm> supportedAlgorithms = | ||
| CompressionAlgorithm.supportedAlgorithms(); | ||
|
|
||
| @Override | ||
| public <RequestT> RequestT modifyBeforeTransmit(RequestHook<?, ?, RequestT> hook) { | ||
|
||
| return hook.mapRequest(HttpRequest.class, RequestCompressionInterceptor::processRequest); | ||
| } | ||
|
|
||
| private static HttpRequest processRequest(RequestHook<?, ?, HttpRequest> hook) { | ||
| if (shouldCompress(hook)) { | ||
| var compressionTrait = | ||
| hook.operation().schema().getTrait(REQUEST_COMPRESSION_TRAIT_KEY); | ||
| var request = hook.request(); | ||
| // Will pick the first supported algorithm to compress the body. | ||
| for (var algorithmId : compressionTrait.getEncodings()) { | ||
| for (var algorithm : supportedAlgorithms) { | ||
| if (algorithmId.equals(algorithm.algorithmId())) { | ||
| var compressed = algorithm.compress(request.body()); | ||
| return request.toBuilder() | ||
| .body(compressed) | ||
| .withAddedHeader(CONTENT_ENCODING_HEADER, algorithmId) | ||
| .build(); | ||
| } | ||
| } | ||
| } | ||
| } | ||
| return hook.request(); | ||
| } | ||
|
|
||
| private static boolean shouldCompress(RequestHook<?, ?, HttpRequest> hook) { | ||
| var context = hook.context(); | ||
| var operation = hook.operation(); | ||
| if (!operation.schema().hasTrait(REQUEST_COMPRESSION_TRAIT_KEY) | ||
| || context.getOrDefault(HttpContext.DISABLE_REQUEST_COMPRESSION, false)) { | ||
| return false; | ||
| } | ||
| var requestBody = hook.request().body(); | ||
| // Streaming should not have known length | ||
| if (operation.inputStreamMember() != null && !requestBody.hasKnownLength()) { | ||
| return true; | ||
| } | ||
| return isBodySizeValid(requestBody, context); | ||
| } | ||
|
|
||
| private static boolean isBodySizeValid(DataStream requestBody, Context context) { | ||
| var minCompressionSize = context.getOrDefault(HttpContext.REQUEST_MIN_COMPRESSION_SIZE_BYTES, | ||
| DEFAULT_MIN_COMPRESSION_SIZE_BYTES); | ||
| validateCompressionSize(minCompressionSize); | ||
| return requestBody.contentLength() >= minCompressionSize; | ||
| } | ||
|
|
||
| private static void validateCompressionSize(int minCompressionSize) { | ||
| if (minCompressionSize < 0 || minCompressionSize > MIN_COMPRESSION_SIZE_CAP) { | ||
| throw new IllegalArgumentException("Min compression size must be between 0 and 10485760"); | ||
joewyz marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
joewyz marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| } | ||
| } | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,3 +1,4 @@ | ||
| software.amazon.smithy.java.client.http.plugins.UserAgentPlugin | ||
| software.amazon.smithy.java.client.http.plugins.ApplyHttpRetryInfoPlugin | ||
| software.amazon.smithy.java.client.http.plugins.RequestCompressionPlugin | ||
| software.amazon.smithy.java.client.http.plugins.HttpChecksumPlugin |
Uh oh!
There was an error while loading. Please reload this page.