Skip to content

Commit a1d8ad3

Browse files
authored
docs(supplemental-docs): md5 checksum fallback for s3 (#6886)
* docs(supplemental-docs): md5 checksum fallback for s3 * test(middleware-flexible-checksums): add e2e test for md5 fallback * docs(supplemental-docs): version update for JSv3 * test(middleware-flexible-checksums): context command update * docs(supplemental-docs): line-wrap md * test(middleware-flexible-checksums): type fix * chore(middleware-flexible-checksums): supplemental docs link
1 parent b7be4ff commit a1d8ad3

File tree

2 files changed

+265
-0
lines changed

2 files changed

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

Diff for: supplemental-docs/MD5_FALLBACK.md

+125
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
# MD5 Checksum Fallback for AWS SDK for JavaScript v3
2+
3+
## Background
4+
5+
In AWS SDK for JavaScript [v3.729.0](https://github.com/aws/aws-sdk-js-v3/releases/tag/v3.729.0),
6+
we shipped a feature that [changed default object integrity in
7+
S3](https://github.com/aws/aws-sdk-js-v3/issues/6810). The SDKs now default to using more modern
8+
checksums (like CRC32) to ensure object integrity, whereas previously MD5 checksums were being used.
9+
Some third-party S3-compatible services currently do not support these checksums. To our knowledge,
10+
this affects only the S3 `DeleteObjects` operation.
11+
12+
If you wish to fallback to the old behavior of sending MD5 checksums, for operations like
13+
`DeleteObjectsCommand` this is how you can do it in AWS SDK for JavaScript v3:
14+
15+
## MD5 fallback
16+
17+
The following code provides a custom S3 client that will use MD5 checksums for DeleteObjects
18+
operations while maintaining the default behavior for all other operations.
19+
20+
```javascript
21+
// md5ClientS3.mjs
22+
import { S3Client } from "@aws-sdk/client-s3";
23+
import { createHash } from "crypto";
24+
25+
/**
26+
* Creates an S3 client that uses MD5 checksums for DeleteObjects operations
27+
*/
28+
export function createS3ClientWithMD5() {
29+
const client = new S3Client({});
30+
const md5Hash = createHash("md5");
31+
32+
client.middlewareStack.add(
33+
(next, context) => async (args) => {
34+
// Check if this is a DeleteObjects command
35+
const isDeleteObjects = context.commandName === "DeleteObjectsCommand";
36+
37+
if (!isDeleteObjects) {
38+
return next(args);
39+
}
40+
41+
const result = await next(args);
42+
const headers = args.request.headers;
43+
44+
// Remove any checksum headers
45+
Object.keys(headers).forEach((header) => {
46+
if (
47+
header.toLowerCase().startsWith("x-amz-checksum-") ||
48+
header.toLowerCase().startsWith("x-amz-sdk-checksum-")
49+
) {
50+
delete headers[header];
51+
}
52+
});
53+
54+
// Add MD5
55+
if (args.request.body) {
56+
const bodyContent = Buffer.from(args.request.body);
57+
headers["Content-MD5"] = md5Hash.update(bodyContent).digest("base64");
58+
}
59+
60+
return result;
61+
},
62+
{
63+
step: "finalizeRequest",
64+
name: "addMD5Checksum",
65+
}
66+
);
67+
68+
return client;
69+
}
70+
```
71+
72+
## Usage
73+
74+
Instead of creating a regular S3 client, use the `createS3ClientWithMD5` function:
75+
76+
```javascript
77+
import { DeleteObjectsCommand } from "@aws-sdk/client-s3";
78+
import { createS3ClientWithMD5 } from "./md5ClientS3.mjs";
79+
80+
// Create the client with MD5 support
81+
const client = createS3ClientWithMD5();
82+
83+
// Use it like a normal S3 client
84+
const deleteParams = {
85+
Bucket: "your-bucket",
86+
Delete: {
87+
Objects: [{ Key: "file1.txt" }, { Key: "file2.txt" }],
88+
},
89+
};
90+
91+
try {
92+
const response = await client.send(new DeleteObjectsCommand(deleteParams));
93+
console.log("Successfully deleted objects:", response);
94+
} catch (err) {
95+
console.error("Error:", err);
96+
}
97+
```
98+
99+
## How It Works
100+
101+
The solution adds middleware to the S3 client that:
102+
103+
1. Detects DeleteObjects operations using the command name
104+
2. Lets the SDK add its default headers
105+
3. Removes any checksum headers in the finalizeRequest step
106+
4. Calculates an MD5 hash of the request body
107+
5. Adds the MD5 hash as a Content-MD5 header
108+
109+
This sequence ensures that we properly replace the checksums with MD5 checksum.
110+
111+
## Usage Notes
112+
113+
- The client can be configured with additional options as needed (region, credentials, etc.)
114+
- If your S3-compatible service supports the SDK's new checksum options or adds support in the
115+
future, you should use the standard S3 client instead.
116+
117+
## Debugging
118+
119+
To verify that the MD5 checksum is being correctly applied, you can add console logging to the
120+
middleware by modifying the code to include logging statements:
121+
122+
```javascript
123+
// Inside the middleware function, add:
124+
console.log("Headers:", JSON.stringify(args.request.headers, null, 2));
125+
```

0 commit comments

Comments
 (0)