Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/aws-cpp-sdk-core/include/aws/core/client/AWSClient.h
Original file line number Diff line number Diff line change
Expand Up @@ -364,6 +364,7 @@ namespace Aws
std::shared_ptr<smithy::client::UserAgentInterceptor> m_userAgentInterceptor;
Aws::Vector<std::shared_ptr<smithy::interceptor::Interceptor>> m_interceptors;
bool m_enableNewRetries;
bool m_disableExpectHeader;
};

AWS_CORE_API Aws::String GetAuthorizationHeader(const Aws::Http::HttpRequest& httpRequest);
Expand Down
17 changes: 10 additions & 7 deletions src/aws-cpp-sdk-core/include/aws/core/client/ClientConfiguration.h
Original file line number Diff line number Diff line change
Expand Up @@ -352,13 +352,16 @@ namespace Aws
FollowRedirectsPolicy followRedirects;

/**
* Only works for Curl http client.
* Curl will by default add "Expect: 100-Continue" header in a Http request so as to avoid sending http
* payload to wire if server respond error immediately after receiving the header.
* Set this option to true will tell Curl to send http request header and body together.
* This can save one round-trip time and especially useful when the payload is small and network latency is more important.
* But be careful when Http request has large payload such S3 PutObject. You don't want to spend long time sending a large payload just getting a error response for server.
* The default value will be false.
* When set to true, the SDK will NOT add "Expect: 100-Continue" header to any HTTP request,
* including streaming operations like S3 PutObject or UploadPart.
*
* By default (false), the SDK automatically adds "Expect: 100-Continue" only to streaming
* requests (operations with streaming input bodies). This allows the server to reject the
* request before the client sends a potentially large payload, saving bandwidth on failures.
* Non-streaming requests never receive the Expect header regardless of this setting.
*
* Set to true if you are going through a proxy or middleware that does not correctly
* handle the Expect: 100-Continue mechanism.
*/
bool disableExpectHeader = false;

Expand Down
2 changes: 2 additions & 0 deletions src/aws-cpp-sdk-core/include/aws/core/http/HttpRequest.h
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,8 @@ namespace Aws
extern AWS_CORE_API const char X_AMZN_TRACE_ID_HEADER[];
extern AWS_CORE_API const char CHUNKED_VALUE[];
extern AWS_CORE_API const char AWS_CHUNKED_VALUE[];
extern AWS_CORE_API const char EXPECT_HEADER[];
extern AWS_CORE_API const char EXPECT_100_CONTINUE_VALUE[];
extern AWS_CORE_API const char X_AMZN_ERROR_TYPE[];
extern AWS_CORE_API const char X_AMZN_QUERY_MODE[];

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,6 @@ class AWS_CORE_API CurlHttpClient: public HttpClient
Aws::String m_caFile;
Aws::String m_proxyCaPath;
Aws::String m_proxyCaFile;
bool m_disableExpectHeader = false;
bool m_allowRedirects = false;
bool m_enableHttpClientTrace = false;
Aws::Http::TransferLibPerformanceMode m_perfMode = TransferLibPerformanceMode::LOW_LATENCY;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ AWSAuthV4Signer::AWSAuthV4Signer(const std::shared_ptr<Auth::AWSCredentialsProvi
m_credentialsProvider(credentialsProvider),
m_serviceName(serviceName),
m_region(region),
m_unsignedHeaders({USER_AGENT, Aws::Auth::AWSAuthHelper::X_AMZN_TRACE_ID, TRANSFER_ENCODING_HEADER}),
m_unsignedHeaders({USER_AGENT, Aws::Auth::AWSAuthHelper::X_AMZN_TRACE_ID, TRANSFER_ENCODING_HEADER, EXPECT_HEADER}),
m_payloadSigningPolicy(signingPolicy),
m_urlEscapePath(urlEscapePath) {
// go ahead and warm up the signing cache.
Expand Down
13 changes: 10 additions & 3 deletions src/aws-cpp-sdk-core/source/client/AWSClient.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,8 @@ AWSClient::AWSClient(const Aws::Client::ClientConfiguration& configuration,
m_httpClient->IsDefaultAwsHttpClient() ? Aws::Client::HttpClientChunkedMode::DEFAULT : configuration.httpClientChunkedMode,
configuration.awsChunkedBufferSize),
m_userAgentInterceptor},
m_enableNewRetries{Aws::Utils::StringUtils::ToLower(Aws::Environment::GetEnv("AWS_NEW_RETRIES_2026").c_str()) == "true"}
m_enableNewRetries{Aws::Utils::StringUtils::ToLower(Aws::Environment::GetEnv("AWS_NEW_RETRIES_2026").c_str()) == "true"},
m_disableExpectHeader(configuration.disableExpectHeader)
{
}

Expand All @@ -170,11 +171,12 @@ AWSClient::AWSClient(const Aws::Client::ClientConfiguration& configuration,
m_enableClockSkewAdjustment(configuration.enableClockSkewAdjustment),
m_requestCompressionConfig(configuration.requestCompressionConfig),
m_userAgentInterceptor{Aws::MakeShared<smithy::client::UserAgentInterceptor>(AWS_CLIENT_LOG_TAG, configuration, m_retryStrategy->GetStrategyName(), m_serviceName)},
m_interceptors{Aws::MakeShared<smithy::client::ChecksumInterceptor>(AWS_CLIENT_LOG_TAG, configuration), Aws::MakeShared<smithy::client::features::ChunkingInterceptor>(AWS_CLIENT_LOG_TAG,
m_interceptors{Aws::MakeShared<smithy::client::ChecksumInterceptor>(AWS_CLIENT_LOG_TAG, configuration), Aws::MakeShared<smithy::client::features::ChunkingInterceptor>(AWS_CLIENT_LOG_TAG,
m_httpClient->IsDefaultAwsHttpClient() ? Aws::Client::HttpClientChunkedMode::DEFAULT : configuration.httpClientChunkedMode,
configuration.awsChunkedBufferSize),
m_userAgentInterceptor},
m_enableNewRetries{Aws::Utils::StringUtils::ToLower(Aws::Environment::GetEnv("AWS_NEW_RETRIES_2026").c_str()) == "true"}
m_enableNewRetries{Aws::Utils::StringUtils::ToLower(Aws::Environment::GetEnv("AWS_NEW_RETRIES_2026").c_str()) == "true"},
m_disableExpectHeader(configuration.disableExpectHeader)
{
}

Expand Down Expand Up @@ -953,6 +955,11 @@ void AWSClient::BuildHttpRequest(const Aws::AmazonWebServiceRequest& request, co
request.AddUserAgentFeature(Aws::Client::UserAgentFeature::PROTOCOL_RPC_V2_CBOR);
}

if (request.IsStreaming() && !m_disableExpectHeader)
{
httpRequest->SetHeaderValue(Aws::Http::EXPECT_HEADER, Aws::Http::EXPECT_100_CONTINUE_VALUE);
}

// Pass along handlers for processing data sent/received in bytes
httpRequest->SetHeadersReceivedEventHandler(request.GetHeadersReceivedEventHandler());
httpRequest->SetDataReceivedEventHandler(request.GetDataReceivedEventHandler());
Expand Down
2 changes: 2 additions & 0 deletions src/aws-cpp-sdk-core/source/http/HttpRequest.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@ namespace Aws
const char AWS_CHUNKED_VALUE[] = "aws-chunked";
const char X_AMZN_TRACE_ID_HEADER[] = "X-Amzn-Trace-Id";
const char ALLOCATION_TAG[] = "HttpRequestConversion";
const char EXPECT_HEADER[] = "expect";
const char EXPECT_100_CONTINUE_VALUE[] = "100-continue";
const char X_AMZN_ERROR_TYPE[] = "x-amzn-errortype";
const char X_AMZN_QUERY_MODE[] = "x-amzn-query-mode";
std::shared_ptr<Aws::Crt::Http::HttpRequest> HttpRequest::ToCrtHttpRequest()
Expand Down
4 changes: 1 addition & 3 deletions src/aws-cpp-sdk-core/source/http/curl/CurlHttpClient.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -610,7 +610,6 @@ CurlHttpClient::CurlHttpClient(const ClientConfiguration& clientConfig) :
m_proxyKeyPasswd(clientConfig.proxySSLKeyPassword),
m_proxyPort(clientConfig.proxyPort), m_verifySSL(clientConfig.verifySSL), m_revokeBestEffort(clientConfig.curlOptions.revokeBestEffort), m_caPath(clientConfig.caPath),
m_caFile(clientConfig.caFile), m_proxyCaPath(clientConfig.proxyCaPath), m_proxyCaFile(clientConfig.proxyCaFile),
m_disableExpectHeader(clientConfig.disableExpectHeader),
m_enableHttpClientTrace(clientConfig.enableHttpClientTrace || FORCE_ENABLE_CURL_LOGGING),
m_perfMode(clientConfig.httpLibPerfMode),
m_telemetryProvider(clientConfig.telemetryProvider)
Expand Down Expand Up @@ -685,8 +684,7 @@ std::shared_ptr<HttpResponse> CurlHttpClient::MakeRequest(const std::shared_ptr<
headers = curl_slist_append(headers, "content-type:");
}

// Discard Expect header so as to avoid using multiple payloads to send a http request (header + body)
if (m_disableExpectHeader)
if (!request->HasHeader(Http::EXPECT_HEADER))
{
headers = curl_slist_append(headers, "Expect:");
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,11 @@ AwsSmithyClientBase::BuildHttpRequest(const std::shared_ptr<AwsSmithyClientAsync
}
}

if (pRequest->IsStreaming() && !m_clientConfig->disableExpectHeader)
{
httpRequest->SetHeaderValue(Aws::Http::EXPECT_HEADER, Aws::Http::EXPECT_100_CONTINUE_VALUE);
}

// Pass along handlers for processing data sent/received in bytes
httpRequest->SetHeadersReceivedEventHandler(pRequest->GetHeadersReceivedEventHandler());
httpRequest->SetDataReceivedEventHandler(pRequest->GetDataReceivedEventHandler());
Expand Down
27 changes: 26 additions & 1 deletion tests/aws-cpp-sdk-dynamodb-unit-tests/DynamoDBUnitTests.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
#include <aws/core/utils/FileSystemUtils.h>
#include <aws/dynamodb/DynamoDBClient.h>
#include <aws/dynamodb/DynamoDBClientConfiguration.h>
#include <aws/dynamodb/model/PutItemRequest.h>
#include <aws/dynamodb/DynamoDBEndpointProvider.h>
#include <aws/testing/AwsTestHelpers.h>
#include <aws/testing/MemoryTesting.h>
Expand Down Expand Up @@ -807,4 +808,28 @@ TEST_F(DynamoDBUnitTest, ShouldWorkWhenAccountIDPresentAndRequired)
EXPECT_TRUE(std::find(features.begin(), features.end(), "R") != features.end());
// Identity resolved a accountId
EXPECT_TRUE(std::find(features.begin(), features.end(), "T") != features.end());
}
}

TEST_F(DynamoDBUnitTest, PutItemDoesNotSendExpect100ContinueHeader) {
using namespace Aws::DynamoDB::Model;

auto putItemRequest = PutItemRequest();
putItemRequest.SetTableName("TestTable");
AttributeValue hashKey;
hashKey.SetS("testKey");
putItemRequest.AddItem("id", hashKey);

auto mockStream = Aws::MakeShared<StandardHttpRequest>(LOG_TAG, "mockuri", HttpMethod::HTTP_GET);
mockStream->SetResponseStreamFactory([]() -> IOStream* {
return Aws::New<StringStream>(LOG_TAG, "{}", std::ios_base::in | std::ios_base::binary);
});
auto successResponse = Aws::MakeShared<StandardHttpResponse>(LOG_TAG, mockStream);
successResponse->SetResponseCode(HttpResponseCode::OK);
mock_http_client_->AddResponseToReturn(successResponse);

const auto putItemOutcome = client_->PutItem(putItemRequest);
EXPECT_TRUE(putItemOutcome.IsSuccess());

const auto requestSeen = mock_http_client_->GetMostRecentHttpRequest();
EXPECT_FALSE(requestSeen.HasHeader("expect"));
}
Original file line number Diff line number Diff line change
Expand Up @@ -2032,6 +2032,33 @@ namespace
listObjectsOutcome = globalClient->ListObjects(listObjectsRequest);
AWS_ASSERT_SUCCESS(listObjectsOutcome);

// Cross-region PutObject (streaming request with Expect: 100-continue).
// Verifies that 301 redirect + Expect interaction works correctly.
PutObjectRequest putObjectRequest;
putObjectRequest.SetBucket(fullBucketName);
putObjectRequest.SetKey("cross-region-test-key");
auto body = Aws::MakeShared<StringStream>(ALLOCATION_TAG, "cross-region body");
putObjectRequest.SetBody(body);
PutObjectOutcome putObjectOutcome = globalClient->PutObject(putObjectRequest);
AWS_ASSERT_SUCCESS(putObjectOutcome);

// Verify the object was written correctly.
GetObjectRequest getObjectRequest;
getObjectRequest.SetBucket(fullBucketName);
getObjectRequest.SetKey("cross-region-test-key");
GetObjectOutcome getObjectOutcome = globalClient->GetObject(getObjectRequest);
AWS_ASSERT_SUCCESS(getObjectOutcome);
Aws::StringStream getObjectBody;
getObjectBody << getObjectOutcome.GetResult().GetBody().rdbuf();
EXPECT_EQ("cross-region body", getObjectBody.str());

// Clean up the object before deleting the bucket.
DeleteObjectRequest deleteObjectRequest;
deleteObjectRequest.SetBucket(fullBucketName);
deleteObjectRequest.SetKey("cross-region-test-key");
DeleteObjectOutcome deleteObjectOutcome = globalClient->DeleteObject(deleteObjectRequest);
AWS_ASSERT_SUCCESS(deleteObjectOutcome);

DeleteBucketRequest deleteBucketRequest;
deleteBucketRequest.SetBucket(fullBucketName);
DeleteBucketOutcome deleteBucketOutcome = globalClient->DeleteBucket(deleteBucketRequest);
Expand Down
129 changes: 129 additions & 0 deletions tests/aws-cpp-sdk-s3-unit-tests/S3UnitTests.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
#include <aws/s3/model/HeadBucketRequest.h>
#include <aws/s3/model/ListObjectsV2Request.h>
#include <aws/s3/model/PutObjectRequest.h>
#include <aws/s3/model/UploadPartRequest.h>
#include <aws/testing/AwsTestHelpers.h>
#include <aws/testing/MemoryTesting.h>
#include <aws/testing/mocks/http/MockHttpClient.h>
Expand Down Expand Up @@ -726,3 +727,131 @@ TEST_F(S3UnitTest, TestGetObjectTimeoutShouldReturnTimeoutError) {
EXPECT_EQ(error.GetResponseCode(), HttpResponseCode::NETWORK_CONNECT_TIMEOUT);
EXPECT_TRUE(error.ShouldRetry());
}

TEST_F(S3UnitTest, PutObjectSendsExpect100ContinueHeader) {
auto putObjectRequest = PutObjectRequest()
.WithBucket("test-bucket")
.WithKey("test-key");

std::shared_ptr<IOStream> body = Aws::MakeShared<StringStream>(ALLOCATION_TAG,
"test body content",
std::ios_base::in | std::ios_base::binary);
putObjectRequest.SetBody(body);

auto mockRequest = Aws::MakeShared<Standard::StandardHttpRequest>(ALLOCATION_TAG, "mockuri", HttpMethod::HTTP_GET);
mockRequest->SetResponseStreamFactory([]() -> IOStream* {
return Aws::New<StringStream>(ALLOCATION_TAG, "response", std::ios_base::in | std::ios_base::binary);
});
auto mockResponse = Aws::MakeShared<Standard::StandardHttpResponse>(ALLOCATION_TAG, mockRequest);
mockResponse->SetResponseCode(HttpResponseCode::OK);
_mockHttpClient->AddResponseToReturn(mockResponse);

const auto response = _s3Client->PutObject(putObjectRequest);
AWS_EXPECT_SUCCESS(response);

const auto seenRequest = _mockHttpClient->GetMostRecentHttpRequest();
EXPECT_TRUE(seenRequest.HasHeader("expect"));
EXPECT_EQ("100-continue", seenRequest.GetHeaderValue("expect"));
}

TEST_F(S3UnitTest, UploadPartSendsExpect100ContinueHeader) {
auto uploadPartRequest = UploadPartRequest()
.WithBucket("test-bucket")
.WithKey("test-key")
.WithUploadId("test-upload-id")
.WithPartNumber(1);

std::shared_ptr<IOStream> body = Aws::MakeShared<StringStream>(ALLOCATION_TAG,
"test body content",
std::ios_base::in | std::ios_base::binary);
uploadPartRequest.SetBody(body);

auto mockRequest = Aws::MakeShared<Standard::StandardHttpRequest>(ALLOCATION_TAG, "mockuri", HttpMethod::HTTP_GET);
mockRequest->SetResponseStreamFactory([]() -> IOStream* {
return Aws::New<StringStream>(ALLOCATION_TAG, "response", std::ios_base::in | std::ios_base::binary);
});
auto mockResponse = Aws::MakeShared<Standard::StandardHttpResponse>(ALLOCATION_TAG, mockRequest);
mockResponse->SetResponseCode(HttpResponseCode::OK);
_mockHttpClient->AddResponseToReturn(mockResponse);

const auto response = _s3Client->UploadPart(uploadPartRequest);
AWS_EXPECT_SUCCESS(response);

const auto seenRequest = _mockHttpClient->GetMostRecentHttpRequest();
EXPECT_TRUE(seenRequest.HasHeader("expect"));
EXPECT_EQ("100-continue", seenRequest.GetHeaderValue("expect"));
}

TEST_F(S3UnitTest, GetObjectDoesNotSendExpect100ContinueHeader) {
auto getObjectRequest = GetObjectRequest()
.WithBucket("test-bucket")
.WithKey("test-key");

auto mockRequest = Aws::MakeShared<Standard::StandardHttpRequest>(ALLOCATION_TAG, "mockuri", HttpMethod::HTTP_GET);
mockRequest->SetResponseStreamFactory([]() -> IOStream* {
return Aws::New<StringStream>(ALLOCATION_TAG, "response body", std::ios_base::in | std::ios_base::binary);
});
auto mockResponse = Aws::MakeShared<Standard::StandardHttpResponse>(ALLOCATION_TAG, mockRequest);
mockResponse->SetResponseCode(HttpResponseCode::OK);
_mockHttpClient->AddResponseToReturn(mockResponse);

const auto response = _s3Client->GetObject(getObjectRequest);
AWS_EXPECT_SUCCESS(response);

const auto seenRequest = _mockHttpClient->GetMostRecentHttpRequest();
EXPECT_FALSE(seenRequest.HasHeader("expect"));
}

TEST_F(S3UnitTest, ListObjectsV2DoesNotSendExpect100ContinueHeader) {
auto listRequest = ListObjectsV2Request()
.WithBucket("test-bucket");

auto mockRequest = Aws::MakeShared<Standard::StandardHttpRequest>(ALLOCATION_TAG, "mockuri", HttpMethod::HTTP_GET);
mockRequest->SetResponseStreamFactory([]() -> IOStream* {
return Aws::New<StringStream>(ALLOCATION_TAG,
"<?xml version=\"1.0\" encoding=\"UTF-8\"?><ListBucketResult xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\"><Name>test-bucket</Name><IsTruncated>false</IsTruncated></ListBucketResult>",
std::ios_base::in | std::ios_base::binary);
});
auto mockResponse = Aws::MakeShared<Standard::StandardHttpResponse>(ALLOCATION_TAG, mockRequest);
mockResponse->SetResponseCode(HttpResponseCode::OK);
_mockHttpClient->AddResponseToReturn(mockResponse);

const auto response = _s3Client->ListObjectsV2(listRequest);
AWS_EXPECT_SUCCESS(response);

const auto seenRequest = _mockHttpClient->GetMostRecentHttpRequest();
EXPECT_FALSE(seenRequest.HasHeader("expect"));
}

TEST_F(S3UnitTest, PutObjectWithDisableExpectHeaderDoesNotSendExpect) {
AWSCredentials credentials{"mock", "credentials"};
const auto epProvider = Aws::MakeShared<S3EndpointProvider>(ALLOCATION_TAG);
S3ClientConfiguration s3Config;
s3Config.region = "us-east-1";
s3Config.retryStrategy = Aws::MakeShared<NoRetry>(ALLOCATION_TAG);
s3Config.disableExpectHeader = true;
auto client = Aws::MakeShared<S3TestClient>(ALLOCATION_TAG, credentials, epProvider, s3Config);

auto putObjectRequest = PutObjectRequest()
.WithBucket("test-bucket")
.WithKey("test-key");

std::shared_ptr<IOStream> body = Aws::MakeShared<StringStream>(ALLOCATION_TAG,
"test body content",
std::ios_base::in | std::ios_base::binary);
putObjectRequest.SetBody(body);

auto mockRequest = Aws::MakeShared<Standard::StandardHttpRequest>(ALLOCATION_TAG, "mockuri", HttpMethod::HTTP_GET);
mockRequest->SetResponseStreamFactory([]() -> IOStream* {
return Aws::New<StringStream>(ALLOCATION_TAG, "response", std::ios_base::in | std::ios_base::binary);
});
auto mockResponse = Aws::MakeShared<Standard::StandardHttpResponse>(ALLOCATION_TAG, mockRequest);
mockResponse->SetResponseCode(HttpResponseCode::OK);
_mockHttpClient->AddResponseToReturn(mockResponse);

const auto response = client->PutObject(putObjectRequest);
AWS_EXPECT_SUCCESS(response);

const auto seenRequest = _mockHttpClient->GetMostRecentHttpRequest();
EXPECT_FALSE(seenRequest.HasHeader("expect"));
}
Loading