Skip to content

AmazonS3Client.PutObjectAsync does not retry with HttpClient HttpCompletionOption.ResponseHeadersRead when using devproxy #3328

Open
@TomKirbz

Description

@TomKirbz

Describe the bug

I'm currently working on a project that exposes the AmazonS3Config retry properties to make them configurable to the client.

To test AWS's retry functionality I've been using a tool called devproxy which "is a command line tool for simulating APIs for testing apps."

With the following code I am able to successfully test this functionality and simulate the AWS API returning 503 and the API will happily retry with throttling and upload my file after a few simulated failed attempts.

AmazonS3Config amazonS3Config = new();
amazonS3Config.Timeout = TimeSpan.FromSeconds(10);
amazonS3Config.RetryMode = RequestRetryMode.Standard;
amazonS3Config.MaxErrorRetry = 10;
amazonS3Config.ThrottleRetries = true;

amazonS3Client amazonS3Client = new(
    myKey
    mySecret,
    myToken,
    amazonS3Config);

Func<Task<Uri>> source = GetDownloadFileUrl();

HttpClient client = _httpClientFactory.CreateClient();
HttpResponseMessage? sourceResponse = await client.GetAsync(await source(), cancellationToken);
using(Stream stream = await sourceResponse.Content.ReadAsStreamAsync(cancellationToken))
{
    PutObjectRequest putRequest = new PutObjectRequest
    {
        BucketName = _bucketName,
        Key = blobName,
        InputStream = stream,
    };

    putRequest.Headers.ContentLength = sourceResponse.Content.Headers.ContentLength ?? -1;
    await _amazonS3Client.PutObjectAsync(putRequest, cancellationToken);
}

However, for memory concerns regarding large files from our target location into the S3 bucket, I have added the following enum to the HttpClient, so the response content is correctly streamed via IAmazonS3.PutObjectAsync and not in memory:

HttpResponseMessage? sourceResponse = await client.GetAsync(await source(), HttpCompletionOption.ResponseHeadersRead, cancellationToken);

With this addition the API no longer retries, if a 503 is returned, a AmazonS3Exception is immediately thrown and does not retry.
This could be an issue with the devproxy tool I am using however if I remove the enum above, the app will continue to retry as expected.

Expected Behavior

The API should retry as expected.

Current Behavior

AmazonS3Exception is immediately thown if a 503 is returned and does not retry.

Reproduction Steps

Attempt to upload a file with the following stream and the config above, and simulate a 503 response:

HttpClient client = _httpClientFactory.CreateClient();
HttpResponseMessage? sourceResponse = await client.GetAsync(<yourUri>, HttpCompletionOption.ResponseHeadersRead, <yourCancellationToken>);
Stream stream = await sourceResponse.Content.ReadAsStreamAsync(<yourCancellationToken>);

Possible Solution

No response

Additional Information/Context

I don't know if these will be of any use but these are the logs from devproxy.

Log1 you can see the api attempts to upload my file. A 503 is returned and my app throws a AmazonS3Exception and continues making a request to localhost to fail the command. This is with the HttpClient having the HttpCompletionOption.ResponseHeadersRead setting:
log1

Log 2
You can see it attempts to do the upload several times before passing the response and my app continues to localhost to pass the command. This is without the HttpCompletionOption.ResponseHeadersRead setting:
log2

Full exception:
Amazon.S3.AmazonS3Exception: Error making request with Error Code ServiceUnavailable and Http Status Code ServiceUnavailable. No further error information was returned by the service. Response Body: { "message": "Slow Down", "details": "The server is currently unable to handle the request due to a temporary overload or maintenance. Please try again later." } ---> Amazon.Runtime.Internal.HttpErrorResponseException: Exception of type 'Amazon.Runtime.Internal.HttpErrorResponseException' was thrown. at Amazon.Runtime.HttpWebRequestMessage.ProcessHttpResponseMessage(HttpResponseMessage responseMessage) at Amazon.Runtime.HttpWebRequestMessage.GetResponseAsync(CancellationToken cancellationToken) at Amazon.Runtime.Internal.HttpHandler1.InvokeAsync[T](IExecutionContext executionContext) at Amazon.Runtime.Internal.RedirectHandler.InvokeAsync[T](IExecutionContext executionContext) at Amazon.Runtime.Internal.Unmarshaller.InvokeAsync[T](IExecutionContext executionContext) at Amazon.S3.Internal.AmazonS3ResponseHandler.InvokeAsync[T](IExecutionContext executionContext) at Amazon.Runtime.Internal.ErrorHandler.InvokeAsync[T](IExecutionContext executionContext) --- End of inner exception stack trace --- at Amazon.Runtime.Internal.HttpErrorResponseExceptionHandler.HandleExceptionStream(IRequestContext requestContext, IWebResponseData httpErrorResponse, HttpErrorResponseException exception, Stream responseStream) at Amazon.Runtime.Internal.HttpErrorResponseExceptionHandler.HandleExceptionAsync(IExecutionContext executionContext, HttpErrorResponseException exception) at Amazon.Runtime.Internal.ExceptionHandler1.HandleAsync(IExecutionContext executionContext, Exception exception) at Amazon.Runtime.Internal.ErrorHandler.ProcessExceptionAsync(IExecutionContext executionContext, Exception exception) at Amazon.Runtime.Internal.ErrorHandler.InvokeAsync[T](IExecutionContext executionContext) at Amazon.Runtime.Internal.CallbackHandler.InvokeAsync[T](IExecutionContext executionContext) at Amazon.Runtime.Internal.Signer.InvokeAsync[T](IExecutionContext executionContext) at Amazon.S3.Internal.S3Express.S3ExpressPreSigner.InvokeAsync[T](IExecutionContext executionContext) at Amazon.Runtime.Internal.EndpointDiscoveryHandler.InvokeAsync[T](IExecutionContext executionContext) at Amazon.Runtime.Internal.EndpointDiscoveryHandler.InvokeAsync[T](IExecutionContext executionContext) at Amazon.Runtime.Internal.CredentialsRetriever.InvokeAsync[T](IExecutionContext executionContext) at Amazon.Runtime.Internal.RetryHandler.InvokeAsync[T](IExecutionContext executionContext) at Amazon.Runtime.Internal.RetryHandler.InvokeAsync[T](IExecutionContext executionContext) at Amazon.Runtime.Internal.CallbackHandler.InvokeAsync[T](IExecutionContext executionContext) at Amazon.Runtime.Internal.CallbackHandler.InvokeAsync[T](IExecutionContext executionContext) at Amazon.S3.Internal.AmazonS3ExceptionHandler.InvokeAsync[T](IExecutionContext executionContext) at Amazon.Runtime.Internal.ErrorCallbackHandler.InvokeAsync[T](IExecutionContext executionContext) at Amazon.Runtime.Internal.MetricsHandler.InvokeAsync[T](IExecutionContext executionContext) at AwsStorageService.UploadFileAsync(String blobName, Nullable1 fileSize, Func1 source, CancellationToken cancellationToken)

AWS .NET SDK and/or Package version used

AWSSDK.S3 3.7.308.7

Targeted .NET Platform

.NET 8

Operating System and version

Windows 10

Metadata

Metadata

Assignees

Labels

bugThis issue is a bug.needs-reproductionThis issue needs reproduction.p2This is a standard priority issues3

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions