Skip to content
Open
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
63 changes: 63 additions & 0 deletions lib/lib-storage/src/Upload.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -506,6 +506,69 @@ describe(Upload.name, () => {
expect(PutObjectCommand).toHaveBeenCalledTimes(0);
});
});

it("should not forward PUT-only or object-level Checksum* params to UploadPart", async () => {
const largeBuffer = Buffer.from("#".repeat(MOCK_PART_SIZE + 10));
const upload = new Upload({
params: {
...params,
Body: largeBuffer,
IfNoneMatch: "*",
ContentType: "application/json",
ContentMD5: "object-level-md5-base64==",
ChecksumSHA256: "object-level-sha256-base64==",
SSECustomerAlgorithm: "AES256",
},
partSize: MOCK_PART_SIZE,
client: new S3({}),
});

await upload.done();

expect(UploadPartCommand).toHaveBeenCalledTimes(2);

// PUT-only fields S3 rejects on UploadPart (501 NotImplemented).
expect(UploadPartCommand).not.toHaveBeenCalledWith(expect.objectContaining({ IfNoneMatch: "*" }));
expect(UploadPartCommand).not.toHaveBeenCalledWith(expect.objectContaining({ ContentType: "application/json" }));

// Object-level checksums: forwarding them as per-part values causes BadDigest (#6742).
expect(UploadPartCommand).not.toHaveBeenCalledWith(
expect.objectContaining({ ContentMD5: "object-level-md5-base64==" })
);
expect(UploadPartCommand).not.toHaveBeenCalledWith(
expect.objectContaining({ ChecksumSHA256: "object-level-sha256-base64==" })
);

// UploadPart-valid fields still flow through.
expect(UploadPartCommand).toHaveBeenCalledWith(expect.objectContaining({ SSECustomerAlgorithm: "AES256" }));

// CompleteMultipartUpload still receives IfNoneMatch (valid there per the S3 API).
expect(CompleteMultipartUploadCommand).toHaveBeenCalledWith(expect.objectContaining({ IfNoneMatch: "*" }));
});

it("should preserve object-level params on the single-part PUT path", async () => {
const upload = new Upload({
params: {
...params,
IfNoneMatch: "*",
ContentMD5: "object-level-md5-base64==",
ChecksumSHA256: "object-level-sha256-base64==",
},
client: new S3({}),
});

await upload.done();

expect(PutObjectCommand).toHaveBeenCalledTimes(1);
expect(PutObjectCommand).toHaveBeenCalledWith(
expect.objectContaining({
IfNoneMatch: "*",
ContentMD5: "object-level-md5-base64==",
ChecksumSHA256: "object-level-sha256-base64==",
})
);
expect(UploadPartCommand).toHaveBeenCalledTimes(0);
});
});

it("should add tags to the object if tags have been added PUT", async () => {
Expand Down
24 changes: 20 additions & 4 deletions lib/lib-storage/src/Upload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -306,15 +306,31 @@ export class Upload extends EventEmitter {

this.__validateUploadPart(dataPart);

// Forward only fields valid on UploadPart with the same semantic meaning
// as on PutObject. Two classes are intentionally excluded:
// - PUT-only fields S3 rejects on UploadPart (IfNoneMatch, ContentType,
// Metadata, ACL, StorageClass, ObjectLock*, etc.).
// - Object-level checksums whose value is a whole-object hash and is
// wrong as a per-part value: ContentMD5 and the precomputed Checksum*
// family (ChecksumSHA*/CRC*/CRC64NVME). See #6742.
// ChecksumAlgorithm is a directive (e.g. "SHA256"), not a precomputed
// hash; the SDK checksum middleware computes per-part hashes from it.
const partResult = await this.client.send(
new UploadPartCommand({
...this.params,
Bucket: this.params.Bucket,
Key: this.params.Key,
ChecksumAlgorithm: this.params.ChecksumAlgorithm,
SSECustomerAlgorithm: this.params.SSECustomerAlgorithm,
SSECustomerKey: this.params.SSECustomerKey,
SSECustomerKeyMD5: this.params.SSECustomerKeyMD5,
RequestPayer: this.params.RequestPayer,
ExpectedBucketOwner: this.params.ExpectedBucketOwner,
Body: dataPart.data,
PartNumber: dataPart.partNumber,
UploadId: this.uploadId,
// dataPart.data is chunked into a non-streaming buffer
// so the ContentLength from the input should not be used for MPU.
ContentLength: undefined,
UploadId: this.uploadId,
Body: dataPart.data,
PartNumber: dataPart.partNumber,
})
);

Expand Down