Skip to content
Draft
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
11 changes: 11 additions & 0 deletions generator/.DevConfigs/32a12d7c-afc6-4bcf-a2d8-9c49b335b935.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"services": [
{
"serviceName": "S3",
"type": "patch",
"changeLogMessages": [
"Fixed issue calling UploadPart with an unseekable stream and disabling checksum failing."
]
}
]
}
16 changes: 12 additions & 4 deletions sdk/src/Services/S3/Custom/Internal/AmazonS3PostMarshallHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -125,11 +125,11 @@ private static void SetStreamChecksum(UploadPartRequest uploadPartRequest, IRequ
if (uploadPartRequest.InputStream != null)
{
// Wrap input stream in partial wrapper (to upload only part of the stream)
var partialStream = new PartialWrapperStream(uploadPartRequest.InputStream, uploadPartRequest.PartSize.GetValueOrDefault());
var partialStream = GetStreamWithLength(uploadPartRequest.InputStream, uploadPartRequest.PartSize.GetValueOrDefault(), chooseMin: true);
if (partialStream.Length > 0 && !(uploadPartRequest.DisablePayloadSigning ?? false))
request.UseChunkEncoding = uploadPartRequest.UseChunkEncoding;
if (!request.Headers.ContainsKey(HeaderKeys.ContentLengthHeader))
request.Headers.Add(HeaderKeys.ContentLengthHeader, partialStream.Length.ToString(CultureInfo.InvariantCulture));
request.Headers.Add(HeaderKeys.ContentLengthHeader, (partialStream.Length - partialStream.Position).ToString(CultureInfo.InvariantCulture));

request.DisablePayloadSigning = uploadPartRequest.DisablePayloadSigning;
uploadPartRequest.InputStream = partialStream;
Expand Down Expand Up @@ -159,7 +159,7 @@ private static void SetStreamChecksum(PutObjectRequest putObjectRequest, IReques
if (putObjectRequest.InputStream != null)
{
// Wrap the stream in a stream that has a length
var streamWithLength = GetStreamWithLength(putObjectRequest.InputStream, putObjectRequest.Headers.ContentLength);
var streamWithLength = GetStreamWithLength(putObjectRequest.InputStream, putObjectRequest.Headers.ContentLength, chooseMin: false);
if (streamWithLength.Length > 0 && !(putObjectRequest.DisablePayloadSigning ?? false))
request.UseChunkEncoding = putObjectRequest.UseChunkEncoding;
var length = streamWithLength.Length - streamWithLength.Position;
Expand Down Expand Up @@ -190,14 +190,22 @@ private static void SetStreamChecksum(PutObjectRequest putObjectRequest, IReques
/// If the stream supports seeking, returns stream.
/// Otherwise, uses hintLength to create a read-only, non-seekable stream of given length
/// </summary>
private static Stream GetStreamWithLength(Stream baseStream, long hintLength)
private static Stream GetStreamWithLength(Stream baseStream, long hintLength, bool chooseMin)
{
Stream result = baseStream;
bool shouldWrapStream = false;
long length = -1;
try
{
length = baseStream.Length - baseStream.Position;

// If chooseMin is true that means we are uploading a part of a stream and the hintLength
// must be treated as the maximum length to read from the baseStream.
if (chooseMin && length > hintLength)
{
shouldWrapStream = true;
length = hintLength;
}
}
catch (NotSupportedException)
{
Expand Down
130 changes: 130 additions & 0 deletions sdk/test/Services/S3/IntegrationTests/PutUnseekableStreamTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
using System.IO;
using System.Text;
using Microsoft.VisualStudio.TestTools.UnitTesting;

using Amazon.S3;
using Amazon.S3.Model;
using Amazon.S3.Util;
using System.Threading.Tasks;

namespace AWSSDK_DotNet.IntegrationTests.Tests.S3
{
[TestClass]
public class PutUnseekableStreamTests : TestBase<AmazonS3Client>
{
private static string bucketName;

[ClassInitialize()]
public static void Initialize(TestContext a)
{
StreamWriter writer = File.CreateText("PutObjectFile.txt");
writer.Write("This is some sample text.!!");
writer.Close();

Comment on lines +20 to +23
Copy link

Copilot AI Oct 14, 2025

Choose a reason for hiding this comment

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

The created file 'PutObjectFile.txt' is never used in the test methods and should be removed to avoid creating unnecessary files during test execution.

Suggested change
StreamWriter writer = File.CreateText("PutObjectFile.txt");
writer.Write("This is some sample text.!!");
writer.Close();

Copilot uses AI. Check for mistakes.
bucketName = S3TestUtils.CreateBucketWithWait(Client, true);
}

[ClassCleanup]
public static void ClassCleanup()
{
AmazonS3Util.DeleteS3BucketWithObjects(Client, bucketName);
BaseClean();
}

[TestMethod]
[TestCategory("S3")]
public async Task TestPutObject()
{
var stream = new CustomStream(Encoding.UTF8.GetBytes("Hello, S3!"));
var putRequest = new PutObjectRequest
{
BucketName = bucketName,
Key = "put-object-unseekable-test.txt",
InputStream = stream,
DisablePayloadSigning = true
};

await Client.PutObjectAsync(putRequest);

var getRequest = new GetObjectRequest
{
BucketName = bucketName,
Key = "put-object-unseekable-test.txt"
};
using (var getResponse = await Client.GetObjectAsync(getRequest))
{
using (var reader = new StreamReader(getResponse.ResponseStream))
{
var content = reader.ReadToEnd();
Assert.AreEqual("Hello, S3!", content);
}
}
}

[TestMethod]
[TestCategory("S3")]
public async Task TestUploadPart()
{
var stream = new CustomStream(Encoding.UTF8.GetBytes("Hello, S3!"));

var initiateMultipartUploadRequest = new InitiateMultipartUploadRequest
{
BucketName = bucketName,
Key = "upload-part-unseekable-test.txt"
};

var initiateMultipartUploadResponse = await Client.InitiateMultipartUploadAsync(initiateMultipartUploadRequest);

var uploadPartRequest = new UploadPartRequest
{
BucketName = bucketName,
Key = "upload-part-unseekable-test.txt",
UploadId = initiateMultipartUploadResponse.UploadId,
PartNumber = 1,
PartSize = stream.Length,
InputStream = stream,
DisablePayloadSigning = true,
IsLastPart = true,
};


var uploadPartResponse = await Client.UploadPartAsync(uploadPartRequest);

var completeMultipartUploadRequest = new CompleteMultipartUploadRequest
{
BucketName = bucketName,
Key = "upload-part-unseekable-test.txt",
UploadId = initiateMultipartUploadResponse.UploadId
};

completeMultipartUploadRequest.AddPartETags(uploadPartResponse);

await Client.CompleteMultipartUploadAsync(completeMultipartUploadRequest);

var getRequest = new GetObjectRequest
{
BucketName = bucketName,
Key = "upload-part-unseekable-test.txt"
};
using (var getResponse = await Client.GetObjectAsync(getRequest))
{
using (var reader = new StreamReader(getResponse.ResponseStream))
{
var content = reader.ReadToEnd();
Assert.AreEqual("Hello, S3!", content);
}
Comment on lines +69 to +115
Copy link

Copilot AI Oct 14, 2025

Choose a reason for hiding this comment

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

Add error handling for the multipart upload scenario. If the UploadPart or CompleteMultipartUpload operations fail, the multipart upload should be aborted to avoid leaving incomplete uploads in S3.

Suggested change
var initiateMultipartUploadRequest = new InitiateMultipartUploadRequest
{
BucketName = bucketName,
Key = "upload-part-unseekable-test.txt"
};
var initiateMultipartUploadResponse = await Client.InitiateMultipartUploadAsync(initiateMultipartUploadRequest);
var uploadPartRequest = new UploadPartRequest
{
BucketName = bucketName,
Key = "upload-part-unseekable-test.txt",
UploadId = initiateMultipartUploadResponse.UploadId,
PartNumber = 1,
PartSize = stream.Length,
InputStream = stream,
DisablePayloadSigning = true,
IsLastPart = true,
};
var uploadPartResponse = await Client.UploadPartAsync(uploadPartRequest);
var completeMultipartUploadRequest = new CompleteMultipartUploadRequest
{
BucketName = bucketName,
Key = "upload-part-unseekable-test.txt",
UploadId = initiateMultipartUploadResponse.UploadId
};
completeMultipartUploadRequest.AddPartETags(uploadPartResponse);
await Client.CompleteMultipartUploadAsync(completeMultipartUploadRequest);
var getRequest = new GetObjectRequest
{
BucketName = bucketName,
Key = "upload-part-unseekable-test.txt"
};
using (var getResponse = await Client.GetObjectAsync(getRequest))
{
using (var reader = new StreamReader(getResponse.ResponseStream))
{
var content = reader.ReadToEnd();
Assert.AreEqual("Hello, S3!", content);
}
string uploadId = null;
try
{
var initiateMultipartUploadRequest = new InitiateMultipartUploadRequest
{
BucketName = bucketName,
Key = "upload-part-unseekable-test.txt"
};
var initiateMultipartUploadResponse = await Client.InitiateMultipartUploadAsync(initiateMultipartUploadRequest);
uploadId = initiateMultipartUploadResponse.UploadId;
var uploadPartRequest = new UploadPartRequest
{
BucketName = bucketName,
Key = "upload-part-unseekable-test.txt",
UploadId = uploadId,
PartNumber = 1,
PartSize = stream.Length,
InputStream = stream,
DisablePayloadSigning = true,
IsLastPart = true,
};
var uploadPartResponse = await Client.UploadPartAsync(uploadPartRequest);
var completeMultipartUploadRequest = new CompleteMultipartUploadRequest
{
BucketName = bucketName,
Key = "upload-part-unseekable-test.txt",
UploadId = uploadId
};
completeMultipartUploadRequest.AddPartETags(uploadPartResponse);
await Client.CompleteMultipartUploadAsync(completeMultipartUploadRequest);
var getRequest = new GetObjectRequest
{
BucketName = bucketName,
Key = "upload-part-unseekable-test.txt"
};
using (var getResponse = await Client.GetObjectAsync(getRequest))
{
using (var reader = new StreamReader(getResponse.ResponseStream))
{
var content = reader.ReadToEnd();
Assert.AreEqual("Hello, S3!", content);
}
}
}
catch
{
if (uploadId != null)
{
await Client.AbortMultipartUploadAsync(new AbortMultipartUploadRequest
{
BucketName = bucketName,
Key = "upload-part-unseekable-test.txt",
UploadId = uploadId
});
}
throw;

Copilot uses AI. Check for mistakes.
}
}


public class CustomStream : MemoryStream
{
public CustomStream(byte[] buffer) : base(buffer)
{
}

public override bool CanSeek => false;

}
}
}