Skip to content

Commit d34f5a3

Browse files
committed
test(middleware-flexible-checksums): add e2e test for md5 fallback
1 parent 0ec0e96 commit d34f5a3

File tree

2 files changed

+143
-7
lines changed

2 files changed

+143
-7
lines changed
Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
// see supplemental-docs/MD5_FALLBACK for more details
2+
import {
3+
CreateBucketCommand,
4+
DeleteBucketCommand,
5+
DeleteObjectsCommand,
6+
PutObjectCommand,
7+
S3,
8+
} from "@aws-sdk/client-s3";
9+
import { createHash } from "crypto";
10+
import { afterAll, beforeAll, describe, expect, test as it } from "vitest";
11+
12+
describe("S3 MD5 Fallback for DeleteObjects", () => {
13+
let s3: S3;
14+
let Bucket: string;
15+
const testFiles = ["md5-test-1.txt", "md5-test-2.txt"];
16+
17+
beforeAll(async () => {
18+
s3 = new S3({ region: "us-west-2" });
19+
Bucket = `md5-fallback-test-${Date.now()}`;
20+
21+
try {
22+
await s3.send(new CreateBucketCommand({ Bucket }));
23+
await new Promise((resolve) => setTimeout(resolve, 2000));
24+
25+
for (const Key of testFiles) {
26+
await s3.send(
27+
new PutObjectCommand({
28+
Bucket,
29+
Key,
30+
Body: "test content",
31+
})
32+
);
33+
}
34+
} catch (err) {
35+
console.error("Setup failed:", err);
36+
throw err;
37+
}
38+
});
39+
40+
afterAll(async () => {
41+
try {
42+
await s3.send(
43+
new DeleteObjectsCommand({
44+
Bucket,
45+
Delete: {
46+
Objects: testFiles.map((Key) => ({ Key })),
47+
},
48+
})
49+
);
50+
await s3.send(new DeleteBucketCommand({ Bucket }));
51+
} catch (error) {
52+
console.error("Cleanup failed:", error);
53+
}
54+
});
55+
56+
it("should use CRC32 checksum by default for DeleteObjects", async () => {
57+
const response = await s3.send(
58+
new DeleteObjectsCommand({
59+
Bucket,
60+
Delete: {
61+
Objects: [{ Key: testFiles[0] }],
62+
},
63+
})
64+
);
65+
66+
// operation successfully deleted exactly one object (CRC32 being used)
67+
expect(response.Deleted?.length).toBe(1);
68+
});
69+
70+
it("should use MD5 checksum for DeleteObjects with middleware", async () => {
71+
const md5S3Client = new S3({ region: "us-west-2" });
72+
let md5Added = false;
73+
let crc32Removed = false;
74+
75+
md5S3Client.middlewareStack.add(
76+
(next) => async (args) => {
77+
// Check if this is a DeleteObjects command
78+
const isDeleteObjects = args.constructor?.name === "DeleteObjectsCommand" || args.input?.Delete !== undefined;
79+
80+
if (!isDeleteObjects) {
81+
return next(args);
82+
}
83+
84+
const result = await next(args);
85+
86+
const headers = args.request.headers;
87+
88+
// Remove checksum headers
89+
Object.keys(headers).forEach((header) => {
90+
if (
91+
header.toLowerCase().startsWith("x-amz-checksum-") ||
92+
header.toLowerCase().startsWith("x-amz-sdk-checksum-")
93+
) {
94+
delete headers[header];
95+
crc32Removed = true;
96+
}
97+
});
98+
99+
// Add MD5
100+
if (args.request.body) {
101+
const bodyContent = Buffer.from(args.request.body);
102+
const md5Hash = createHash("md5").update(bodyContent).digest("base64");
103+
headers["Content-MD5"] = md5Hash;
104+
md5Added = true;
105+
}
106+
107+
return result;
108+
},
109+
{
110+
step: "finalizeRequest",
111+
name: "addMD5Checksum",
112+
}
113+
);
114+
115+
const response = await md5S3Client.send(
116+
new DeleteObjectsCommand({
117+
Bucket,
118+
Delete: {
119+
Objects: [{ Key: testFiles[1] }],
120+
},
121+
})
122+
);
123+
124+
// If MD5 wasn't properly set, this call will fail
125+
expect(response.Deleted?.length).toBe(1);
126+
expect(md5Added).toBe(true);
127+
expect(crc32Removed).toBe(true);
128+
});
129+
});

supplemental-docs/MD5_FALLBACK.md

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,12 @@ export function createS3ClientWithMD5() {
3030
return next(args);
3131
}
3232

33-
// Remove any checksum headers
33+
const result = await next(args);
34+
35+
// Modify the final request headers
3436
const headers = args.request.headers;
37+
38+
// Remove any checksum headers
3539
Object.keys(headers).forEach((header) => {
3640
if (
3741
header.toLowerCase().startsWith("x-amz-checksum-") ||
@@ -48,10 +52,10 @@ export function createS3ClientWithMD5() {
4852
headers["Content-MD5"] = md5Hash;
4953
}
5054

51-
return next(args);
55+
return result;
5256
},
5357
{
54-
step: "build",
58+
step: "finalizeRequest", // Run after all other request modifications
5559
name: "addMD5Checksum",
5660
}
5761
);
@@ -92,14 +96,17 @@ try {
9296
The solution adds middleware to the S3 client that:
9397
9498
1. Detects DeleteObjects operations
95-
2. Removes any checksum headers
96-
3. Calculates an MD5 hash of the request body (in the `build` step of the request lifecycle, as per the middleware implementation above)
97-
4. Adds the MD5 hash as a Content-MD5 header
99+
2. Lets the SDK add its default headers
100+
3. Removes any checksum headers in the finalizeRequest step
101+
4. Calculates an MD5 hash of the request body
102+
5. Adds the MD5 hash as a Content-MD5 header
103+
104+
This sequence ensures that we properly replace the current checksums with the MD5 checksum.
98105
99106
## Usage Notes
100107
101108
- The client can be configured with additional options as needed (region, credentials, etc.)
102-
- If your S3-compatible service supports the SDK's new checksum options or adds support in the future, you should use the standard S3 client instead
109+
- If your S3-compatible service supports the SDK's new checksum options or adds support in the future, you should use the standard S3 client instead.
103110
104111
## Debugging
105112

0 commit comments

Comments
 (0)