Skip to content

Commit 32853df

Browse files
committed
Donot send Expect 100-continue when RequestBody is of Zero content length
1 parent ba102d6 commit 32853df

File tree

4 files changed

+113
-2
lines changed

4 files changed

+113
-2
lines changed
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"type": "bugfix",
3+
"category": "Amazon S3",
4+
"contributor": "",
5+
"description": "Fix StreamingRequestInterceptor to skip Expect: 100-continue header when PutObject or UploadPart requests have zero content length."
6+
}

services/s3/src/main/java/software/amazon/awssdk/services/s3/internal/handlers/StreamingRequestInterceptor.java

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import software.amazon.awssdk.core.interceptor.Context;
2020
import software.amazon.awssdk.core.interceptor.ExecutionAttributes;
2121
import software.amazon.awssdk.core.interceptor.ExecutionInterceptor;
22+
import software.amazon.awssdk.core.sync.RequestBody;
2223
import software.amazon.awssdk.http.SdkHttpRequest;
2324
import software.amazon.awssdk.services.s3.model.PutObjectRequest;
2425
import software.amazon.awssdk.services.s3.model.UploadPartRequest;
@@ -32,9 +33,38 @@ public final class StreamingRequestInterceptor implements ExecutionInterceptor {
3233
@Override
3334
public SdkHttpRequest modifyHttpRequest(Context.ModifyHttpRequest context,
3435
ExecutionAttributes executionAttributes) {
35-
if (context.request() instanceof PutObjectRequest || context.request() instanceof UploadPartRequest) {
36+
if (shouldAddExpectContinueHeader(context)) {
3637
return context.httpRequest().toBuilder().putHeader("Expect", "100-continue").build();
3738
}
3839
return context.httpRequest();
3940
}
41+
42+
/**
43+
* Determines whether to add 'Expect: 100-continue' header to streaming requests.
44+
*
45+
* Per RFC 9110 Section 10.1.1, clients MUST NOT send 100-continue for requests without content.
46+
*
47+
* Note: Empty Content length check currently applies to sync clients only. Sync HTTP clients (e.g., Apache HttpClient) may
48+
* reuse connections, and sending empty content with Expect header can cause issues if the server has already closed the
49+
* connection.
50+
*
51+
* @param context the HTTP request modification context
52+
* @return true if Expect header should be added, false otherwise
53+
*/
54+
private boolean shouldAddExpectContinueHeader(Context.ModifyHttpRequest context) {
55+
// Must be a streaming request type
56+
if (context.request() instanceof PutObjectRequest
57+
|| context.request() instanceof UploadPartRequest) {
58+
// Zero Content length check
59+
return context.requestBody()
60+
.flatMap(RequestBody::optionalContentLength)
61+
.map(length -> length != 0L)
62+
.orElse(true);
63+
}
64+
return false;
65+
}
66+
67+
68+
69+
4070
}

services/s3/src/test/java/software/amazon/awssdk/services/s3/internal/handlers/StreamingRequestInterceptorTest.java

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626
import software.amazon.awssdk.services.s3.model.UploadPartRequest;
2727

2828
public class StreamingRequestInterceptorTest {
29-
private final StreamingRequestInterceptor interceptor = new StreamingRequestInterceptor();
29+
private StreamingRequestInterceptor interceptor = new StreamingRequestInterceptor();
3030

3131
@Test
3232
public void modifyHttpRequest_setsExpect100Continue_whenSdkRequestIsPutObject() {
@@ -55,4 +55,44 @@ public void modifyHttpRequest_doesNotSetExpect_whenSdkRequestIsNotPutObject() {
5555

5656
assertThat(modifiedRequest.firstMatchingHeader("Expect")).isNotPresent();
5757
}
58+
59+
@Test
60+
public void modifyHttpRequest_doesNotSetExpect_whenPutObjectHasZeroContentLength() {
61+
SdkHttpRequest modifiedRequest = interceptor.modifyHttpRequest(
62+
modifyHttpRequestContext(PutObjectRequest.builder().build(), 0L),
63+
new ExecutionAttributes());
64+
65+
assertThat(modifiedRequest.firstMatchingHeader("Expect"))
66+
.as("Expect header should not be present for zero-length content per RFC 9110")
67+
.isNotPresent();
68+
}
69+
70+
@Test
71+
public void modifyHttpRequest_doesNotSetExpect_whenUploadPartHasZeroContentLength() {
72+
SdkHttpRequest modifiedRequest = interceptor.modifyHttpRequest(
73+
modifyHttpRequestContext(UploadPartRequest.builder().build(), 0L),
74+
new ExecutionAttributes());
75+
76+
assertThat(modifiedRequest.firstMatchingHeader("Expect"))
77+
.as("Expect header should not be present for zero-length content per RFC 9110")
78+
.isNotPresent();
79+
}
80+
81+
@Test
82+
public void modifyHttpRequest_setsExpect_whenPutObjectHasNonZeroContentLength() {
83+
SdkHttpRequest modifiedRequest = interceptor.modifyHttpRequest(
84+
modifyHttpRequestContext(PutObjectRequest.builder().build(), 1024L),
85+
new ExecutionAttributes());
86+
87+
assertThat(modifiedRequest.firstMatchingHeader("Expect")).hasValue("100-continue");
88+
}
89+
90+
@Test
91+
public void modifyHttpRequest_setsExpect_whenUploadPartHasNonZeroContentLength() {
92+
SdkHttpRequest modifiedRequest = interceptor.modifyHttpRequest(
93+
modifyHttpRequestContext(UploadPartRequest.builder().build(), 5242880L),
94+
new ExecutionAttributes());
95+
96+
assertThat(modifiedRequest.firstMatchingHeader("Expect")).hasValue("100-continue");
97+
}
5898
}

services/s3/src/test/java/software/amazon/awssdk/services/s3/utils/InterceptorTestUtils.java

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,41 @@ public SdkRequest request() {
133133
};
134134
}
135135

136+
/**
137+
* Creates a ModifyHttpRequest context with a specific content length.
138+
* Useful for testing scenarios where content length matters (e.g., RFC 9110 compliance).
139+
*
140+
* @param request the SDK request
141+
* @param contentLength the content length in bytes
142+
* @return a ModifyHttpRequest context with the specified content length
143+
*/
144+
public static Context.ModifyHttpRequest modifyHttpRequestContext(SdkRequest request, long contentLength) {
145+
Optional<RequestBody> requestBody = Optional.of(RequestBody.fromBytes(new byte[(int) contentLength]));
146+
Optional<AsyncRequestBody> asyncRequestBody = Optional.of(AsyncRequestBody.fromBytes(new byte[(int) contentLength]));
147+
148+
return new Context.ModifyHttpRequest() {
149+
@Override
150+
public SdkHttpRequest httpRequest() {
151+
return sdkHttpFullRequest();
152+
}
153+
154+
@Override
155+
public Optional<RequestBody> requestBody() {
156+
return requestBody;
157+
}
158+
159+
@Override
160+
public Optional<AsyncRequestBody> asyncRequestBody() {
161+
return asyncRequestBody;
162+
}
163+
164+
@Override
165+
public SdkRequest request() {
166+
return request;
167+
}
168+
};
169+
}
170+
136171
public static Context.ModifyResponse modifyResponseContext(SdkRequest request, SdkResponse response, SdkHttpResponse sdkHttpResponse) {
137172
return new Context.ModifyResponse() {
138173
@Override

0 commit comments

Comments
 (0)