Skip to content

Conversation

@thinaih
Copy link
Member

@thinaih thinaih commented Oct 6, 2025

This PR does Addresses #199

It does provide support for

  • Trailer headers regardless of whether they are part of a signed or unsigned aws chunked request
  • It handles aws chunked unsigned requests

Overall changes

  • Create new types AWS_CHUNKED_UNSIGNED and AWS_CHUNKED_IN_W3C_CHUNKED_UNSIGNED in ContentType
  • Make ChunkSigningSession optional in AwsChunkedInputStream class
  • Support reading the trailer header chunk as part of AwsChunkedInputStream class. Check docs for reference on the trailer headers chunk.

Summary by Sourcery

Add support for AWS chunked payloads that include trailer headers and handle unsigned AWS-chunked streams throughout the proxy stack.

New Features:

  • Support reading and validating trailer header chunks in signed AWS-chunked streams
  • Handle unsigned AWS-chunked payloads with optional trailer headers

Enhancements:

  • Introduce AWS_CHUNKED_UNSIGNED and AWS_CHUNKED_IN_W3C_CHUNKED_UNSIGNED content types
  • Make chunk signing session optional and track configured trailer headers in AwsChunkedInputStream
  • Propagate trailer header lists through RequestHeadersBuilder, RequestContent, RequestBuilder, and TrinoS3ProxyClient

Tests:

  • Extend AwsChunkedInputStream tests for trailer headers, signed and unsigned streams
  • Add HTTP chunked upload tests for unsigned AWS-chunked encoding and trailer header validations
  • Update request headers builder and generic request tests to cover new unsigned AWS-chunked scenarios

@cla-bot cla-bot bot added the cla-signed label Oct 6, 2025
@thinaih thinaih force-pushed the fix-streaming-for-aws-chunked-requests branch from 58504f2 to 3704d9a Compare October 7, 2025 16:38
@thinaih thinaih force-pushed the fix-streaming-for-aws-chunked-requests branch from 042be34 to 8b12b60 Compare October 7, 2025 16:53
@thinaih thinaih marked this pull request as ready for review October 7, 2025 16:53
@thinaih thinaih requested review from Randgalt and vagaerg October 7, 2025 16:53
@sourcery-ai
Copy link

sourcery-ai bot commented Oct 7, 2025

Reviewer's Guide

This PR adds support for AWS chunked unsigned payloads and trailer headers by extending the content-type model, enhancing AwsChunkedInputStream to accept optional signing and parse trailer header chunks, updating header builders and proxy client to propagate trailer metadata, and expanding the test suite to cover trailer and unsigned scenarios.

Class diagram for updated AwsChunkedInputStream and related types

classDiagram
class AwsChunkedInputStream {
  -InputStream delegate
  -Optional<ChunkSigningSession> chunkSigningSession
  -int bytesRemainingInChunk
  -int bytesAccountedFor
  -int decodedContentLength
  -List<String> trailerHeaders
  +AwsChunkedInputStream(InputStream, Optional<ChunkSigningSession>, int, List<String>)
  +read()
  +read(byte[], int, int)
  -nextChunk()
  -readTrailingHeadersChunk()
  -readTrailingHeaders()
}
class ChunkSigningSession {
  +write(byte)
  +write(byte[], int, int)
  +startChunk(String)
  +complete()
}
class TrailerHeaderChunk {
  +String trailerHeaders
  +Optional<String> signature
}
AwsChunkedInputStream --> "0..1" ChunkSigningSession : optional
AwsChunkedInputStream --> "*" TrailerHeaderChunk : uses
Loading

Class diagram for updated ContentType enum in RequestContent

classDiagram
class ContentType {
  <<enum>>
  STANDARD
  W3C_CHUNKED
  AWS_CHUNKED
  AWS_CHUNKED_UNSIGNED
  AWS_CHUNKED_IN_W3C_CHUNKED
  AWS_CHUNKED_IN_W3C_CHUNKED_UNSIGNED
}
class RequestContent {
  +ContentType contentType()
  +Optional<Integer> contentLength()
  +List<String> trailerHeaders()
  +Optional<InputStream> inputStream()
}
RequestContent --> ContentType
Loading

File-Level Changes

Change Details Files
Introduce new content types for unsigned and trailer-based AWS chunked payloads
  • Added AWS_CHUNKED_UNSIGNED and AWS_CHUNKED_IN_W3C_CHUNKED_UNSIGNED enum entries
  • Expanded CHUNKED_CONTENT_TYPES set and mapping logic in RequestHeadersBuilder
  • Allowed x-amz-trailer header in RequestHeadersBuilder parsing
  • Updated TestRequestHeadersBuilder with scenarios for unsigned and trailer cases
trino-aws-proxy-spi/src/main/java/io/trino/aws/proxy/spi/rest/RequestContent.java
trino-aws-proxy-spi/src/main/java/io/trino/aws/proxy/spi/rest/ContentType.java
trino-aws-proxy/src/main/java/io/trino/aws/proxy/server/rest/RequestHeadersBuilder.java
trino-aws-proxy/src/test/java/io/trino/aws/proxy/server/rest/TestRequestHeadersBuilder.java
Enhance AwsChunkedInputStream to handle optional signing sessions and trailer header chunks
  • Changed chunkSigningSession field to Optional and added trailerHeaders parameter
  • Guarded write calls on Optional signing session
  • Implemented readTrailingHeadersChunk, readTrailingHeaders and integrated into nextChunk for trailer processing
  • Introduced TrailerHeaderChunk record and updated state transitions for final chunk
trino-aws-proxy/src/main/java/io/trino/aws/proxy/server/rest/AwsChunkedInputStream.java
Propagate trailer headers and optional signing through request builders and proxy client
  • Extended RequestContent interface with trailerHeaders()
  • Updated RequestBuilder to supply trailerHeaders list and accept AWS_CHUNKED_UNSIGNED types
  • Modified TrinoS3ProxyClient to pass Optional.empty() for unsigned and trailerHeaders to AwsChunkedInputStream
trino-aws-proxy/src/main/java/io/trino/aws/proxy/server/rest/RequestBuilder.java
trino-aws-proxy/src/main/java/io/trino/aws/proxy/server/rest/TrinoS3ProxyClient.java
Adapt chunked-stream generator and basic unit tests for trailer headers and unsigned streams
  • Added Optional trailerHeaders parameter to generateChunkedStream in TestingChunkSigningSession
  • Appended trailer headers and computed trailer signature in stream generator
  • Updated TestAwsChunkedInputStream to pass trailer header names and validate trailer parsing
  • Added tests covering signed trailer, multiple trailers, and unsigned chunked payloads
trino-aws-proxy/src/test/java/io/trino/aws/proxy/server/signing/TestingChunkSigningSession.java
trino-aws-proxy/src/test/java/io/trino/aws/proxy/server/rest/TestAwsChunkedInputStream.java
Extend HTTP-level integration tests to cover unsigned AWS chunked payloads and trailer header scenarios
  • Introduced new TestHttpChunked methods for unsigned payloads, multi-encoding, trailer headers, and failure cases
  • Added helper buildTrailerChunk and generateUnsignedChunkedStream utilities
  • Validated correct status codes and storage metadata for trailer and unsigned uploads
trino-aws-proxy/src/test/java/io/trino/aws/proxy/server/rest/TestHttpChunked.java

Possibly linked issues


Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

Copy link

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey there - I've reviewed your changes and they look great!

Prompt for AI Agents
Please address the comments from this code review:

## Individual Comments

### Comment 1
<location> `trino-aws-proxy/src/main/java/io/trino/aws/proxy/server/rest/AwsChunkedInputStream.java:259` </location>
<code_context>
         return line.toString();
     }
+
+    private TrailerHeaderChunk readTrailingHeadersChunk()
+            throws IOException
+    {
</code_context>

<issue_to_address>
**issue (bug_risk):** The logic for parsing trailer headers may break early if x-amz-trailer-signature is found before all expected headers.

The loop may exit before processing all expected trailer headers if the signature is encountered early. Please verify whether all headers should be read before breaking, or if the signature is always last.
</issue_to_address>

### Comment 2
<location> `trino-aws-proxy/src/main/java/io/trino/aws/proxy/server/rest/AwsChunkedInputStream.java:268-269` </location>
<code_context>
+            String trailerHeaders = readLine();
+            List<String> trailerHeadersValues = Splitter.on(":").trimResults().limit(2).splitToList(trailerHeaders);
+            String trailerHeaderName = trailerHeadersValues.getFirst();
+            if ((trailerHeadersValues.size() != 2) || !this.trailerHeaders.contains(trailerHeaderName)) {
+                throw new WebApplicationException("Trailer header is invalid: " + trailerHeaders, BAD_REQUEST);
+            }
+            if (trailerHeaderName.equals("x-amz-trailer-signature")) {
</code_context>

<issue_to_address>
**suggestion (bug_risk):** The check for trailer header validity may be too strict if header names are case-insensitive.

Normalize both the input and expected header names to lower-case before comparison to ensure case-insensitive matching.

Suggested implementation:

```java
            String trailerHeaders = readLine();
            List<String> trailerHeadersValues = Splitter.on(":").trimResults().limit(2).splitToList(trailerHeaders);
            String trailerHeaderName = trailerHeadersValues.getFirst().toLowerCase();
            boolean validTrailerHeader = this.trailerHeaders.stream()
                .map(String::toLowerCase)
                .anyMatch(h -> h.equals(trailerHeaderName));
            if ((trailerHeadersValues.size() != 2) || !validTrailerHeader) {
                throw new WebApplicationException("Trailer header is invalid: " + trailerHeaders, BAD_REQUEST);
            }
            if (trailerHeaderName.equals("x-amz-trailer-signature")) {

```

```java
            if (trailerHeaderName.equals("x-amz-trailer-signature")) {
                signature = Optional.of(trailerHeadersValues.getLast());
                break;
            }
            else {
                trailerHeadersChunkBuilder.append(trailerHeaders);
            }

```
</issue_to_address>

### Comment 3
<location> `trino-aws-proxy/src/main/java/io/trino/aws/proxy/server/rest/RequestHeadersBuilder.java:206-209` </location>
<code_context>
             passthroughHeadersBuilder.addAll(headerName, headerValues);
         }

+        private String requiredContentSha256()
+        {
+            return contentSha256.orElseThrow(() -> new WebApplicationException(BAD_REQUEST));
</code_context>

<issue_to_address>
**suggestion:** The requiredContentSha256() method throws a generic BAD_REQUEST error without a message.

Include a descriptive error message in the exception to clarify when the x-amz-content-sha256 header is missing or invalid.

```suggestion
        private String requiredContentSha256()
        {
            return contentSha256.orElseThrow(() -> new WebApplicationException(
                "Missing or invalid x-amz-content-sha256 header", BAD_REQUEST));
        }
```
</issue_to_address>

### Comment 4
<location> `trino-aws-proxy/src/main/java/io/trino/aws/proxy/server/rest/RequestBuilder.java:193-194` </location>
<code_context>
+        this.trailerHeaders = requireNonNull(ImmutableList.copyOf(trailerHeaders), "trailerHeaders is null");
     }

     @Override
</code_context>

<issue_to_address>
**issue (bug_risk):** The trailerHeaders() implementation assumes the header is present and non-null.

If the header is missing, ImmutableList.copyOf will throw a NullPointerException. Use Optional.ofNullable or default to an empty list to prevent this.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

return line.toString();
}

private TrailerHeaderChunk readTrailingHeadersChunk()
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (bug_risk): The logic for parsing trailer headers may break early if x-amz-trailer-signature is found before all expected headers.

The loop may exit before processing all expected trailer headers if the signature is encountered early. Please verify whether all headers should be read before breaking, or if the signature is always last.

Comment on lines +268 to +269
if ((trailerHeadersValues.size() != 2) || !this.trailerHeaders.contains(trailerHeaderName)) {
throw new WebApplicationException("Trailer header is invalid: " + trailerHeaders, BAD_REQUEST);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (bug_risk): The check for trailer header validity may be too strict if header names are case-insensitive.

Normalize both the input and expected header names to lower-case before comparison to ensure case-insensitive matching.

Suggested implementation:

            String trailerHeaders = readLine();
            List<String> trailerHeadersValues = Splitter.on(":").trimResults().limit(2).splitToList(trailerHeaders);
            String trailerHeaderName = trailerHeadersValues.getFirst().toLowerCase();
            boolean validTrailerHeader = this.trailerHeaders.stream()
                .map(String::toLowerCase)
                .anyMatch(h -> h.equals(trailerHeaderName));
            if ((trailerHeadersValues.size() != 2) || !validTrailerHeader) {
                throw new WebApplicationException("Trailer header is invalid: " + trailerHeaders, BAD_REQUEST);
            }
            if (trailerHeaderName.equals("x-amz-trailer-signature")) {
            if (trailerHeaderName.equals("x-amz-trailer-signature")) {
                signature = Optional.of(trailerHeadersValues.getLast());
                break;
            }
            else {
                trailerHeadersChunkBuilder.append(trailerHeaders);
            }

Comment on lines +206 to +209
private String requiredContentSha256()
{
return contentSha256.orElseThrow(() -> new WebApplicationException(BAD_REQUEST));
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: The requiredContentSha256() method throws a generic BAD_REQUEST error without a message.

Include a descriptive error message in the exception to clarify when the x-amz-content-sha256 header is missing or invalid.

Suggested change
private String requiredContentSha256()
{
return contentSha256.orElseThrow(() -> new WebApplicationException(BAD_REQUEST));
}
private String requiredContentSha256()
{
return contentSha256.orElseThrow(() -> new WebApplicationException(
"Missing or invalid x-amz-content-sha256 header", BAD_REQUEST));
}

Comment on lines +193 to +194
@Override
public List<String> trailerHeaders()
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (bug_risk): The trailerHeaders() implementation assumes the header is present and non-null.

If the header is missing, ImmutableList.copyOf will throw a NullPointerException. Use Optional.ofNullable or default to an empty list to prevent this.

@vagaerg
Copy link
Member

vagaerg commented Oct 8, 2025

We currently only support signing chunks with header AWS4-HMAC-SHA256-PAYLOAD - see

private static final String CHUNK_STRING_TO_SIGN_PREFIX = "AWS4-HMAC-SHA256-PAYLOAD";

However, for trailer headers this should be AWS4-HMAC-SHA256-TRAILER. The tests currently reuse the same generateChunkedStream function in order to generate a trailer header chunk, which is why they pass I believe. The input into the test is being built on the assumption that chunks use AWS4-HMAC-SHA256-PAYLOAD rather than -TRAILER.

@vagaerg
Copy link
Member

vagaerg commented Oct 8, 2025

Had a chat with @thinaih about the way trailer headers are canonicalized. The docs don't actually mention how to canonicalize trailers, unlike for normal headers where they state they need to be sorted.

However, looking at the implementation of the SDK it seems like they follow the same logic (i.e., they are sorted):

https://github.com/aws/aws-sdk-java-v2/blob/b4e08d298fe0e6e6d4c35f05e24ee781b2a7735d/core/auth/src/main/java/software/amazon/awssdk/auth/signer/internal/chunkedencoding/AwsS3V4ChunkSigner.java#L103-L114

and

https://github.com/aws/aws-sdk-java-v2/blob/b4e08d298fe0e6e6d4c35f05e24ee781b2a7735d/core/auth/src/main/java/software/amazon/awssdk/auth/signer/internal/util/HeaderTransformsHelper.java#L32

Since our current chunk signing class is largely copied from the SDK's chunk signer class, we may want to take inspiration from it as a way to implement signing for trailers properly (and also fix the comment I mentioned above re. AWS4-HMAC-SHA256-PAYLOAD being wrong)

@thinaih
Copy link
Member Author

thinaih commented Oct 8, 2025

We currently only support signing chunks with header AWS4-HMAC-SHA256-PAYLOAD - see

private static final String CHUNK_STRING_TO_SIGN_PREFIX = "AWS4-HMAC-SHA256-PAYLOAD";

However, for trailer headers this should be AWS4-HMAC-SHA256-TRAILER. The tests currently reuse the same generateChunkedStream function in order to generate a trailer header chunk, which is why they pass I believe. The input into the test is being built on the assumption that chunks use AWS4-HMAC-SHA256-PAYLOAD rather than -TRAILER.

Good catch! Will fix it!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Development

Successfully merging this pull request may close these issues.

2 participants