mirror of
https://github.com/oven-sh/bun
synced 2026-02-09 18:38:55 +00:00
## Summary - S3 `File.presign()` was ignoring the `contentDisposition` and `type` options - These options are now properly included as `response-content-disposition` and `response-content-type` query parameters in the presigned URL - Added `content_type` field to `SignOptions` and `S3CredentialsWithOptions` structs - Added parsing for the `type` option in `getCredentialsWithOptions()` - Query parameters are added in correct alphabetical order for AWS Signature V4 compliance ## Test plan - [x] Added regression test in `test/regression/issue/25750.test.ts` - [x] Verified tests pass with debug build: `bun bd test test/regression/issue/25750.test.ts` - [x] Verified tests fail with system bun (without fix): `USE_SYSTEM_BUN=1 bun test test/regression/issue/25750.test.ts` - [x] Verified existing S3 presign tests still pass - [x] Verified existing S3 signature order tests still pass Fixes #25750 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Bot <claude-bot@bun.sh> Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
130 lines
4.1 KiB
TypeScript
130 lines
4.1 KiB
TypeScript
import { S3Client } from "bun";
|
|
import { describe, expect, it } from "bun:test";
|
|
|
|
// Test for GitHub issue #25750: S3 File.presign() ignores contentDisposition and type options
|
|
describe("issue #25750 - S3 presign contentDisposition and type", () => {
|
|
const s3Client = new S3Client({
|
|
region: "us-east-1",
|
|
endpoint: "https://s3.us-east-1.amazonaws.com",
|
|
accessKeyId: "test-key",
|
|
secretAccessKey: "test-secret",
|
|
bucket: "test-bucket",
|
|
});
|
|
|
|
it("should include response-content-disposition in presigned URL", () => {
|
|
const file = s3Client.file("example.txt");
|
|
|
|
const url = file.presign({
|
|
method: "GET",
|
|
expiresIn: 900,
|
|
contentDisposition: 'attachment; filename="quarterly-report.txt"',
|
|
});
|
|
|
|
expect(url).toContain("response-content-disposition=");
|
|
expect(url).toContain("attachment");
|
|
expect(url).toContain("quarterly-report.txt");
|
|
});
|
|
|
|
it("should include response-content-type in presigned URL", () => {
|
|
const file = s3Client.file("example.txt");
|
|
|
|
const url = file.presign({
|
|
method: "GET",
|
|
expiresIn: 900,
|
|
type: "application/octet-stream",
|
|
});
|
|
|
|
expect(url).toContain("response-content-type=");
|
|
expect(url).toContain("application%2Foctet-stream");
|
|
});
|
|
|
|
it("should include both response-content-disposition and response-content-type in presigned URL", () => {
|
|
const file = s3Client.file("example.txt");
|
|
|
|
const url = file.presign({
|
|
method: "GET",
|
|
expiresIn: 900,
|
|
contentDisposition: 'attachment; filename="quarterly-report.txt"',
|
|
type: "application/octet-stream",
|
|
});
|
|
|
|
expect(url).toContain("response-content-disposition=");
|
|
expect(url).toContain("response-content-type=");
|
|
expect(url).toContain("attachment");
|
|
expect(url).toContain("application%2Foctet-stream");
|
|
});
|
|
|
|
it("should work with S3Client.presign static method", () => {
|
|
const url = S3Client.presign("example.txt", {
|
|
region: "us-east-1",
|
|
endpoint: "https://s3.us-east-1.amazonaws.com",
|
|
accessKeyId: "test-key",
|
|
secretAccessKey: "test-secret",
|
|
bucket: "test-bucket",
|
|
contentDisposition: 'attachment; filename="report.pdf"',
|
|
type: "application/pdf",
|
|
expiresIn: 3600,
|
|
});
|
|
|
|
expect(url).toContain("response-content-disposition=");
|
|
expect(url).toContain("response-content-type=");
|
|
expect(url).toContain("report.pdf");
|
|
expect(url).toContain("application%2Fpdf");
|
|
});
|
|
|
|
it("should properly URL-encode special characters in contentDisposition", () => {
|
|
const file = s3Client.file("test.txt");
|
|
|
|
const url = file.presign({
|
|
method: "GET",
|
|
contentDisposition: 'attachment; filename="file with spaces & symbols.txt"',
|
|
});
|
|
|
|
expect(url).toContain("response-content-disposition=");
|
|
// Special characters should be URL encoded
|
|
expect(url).toContain("%20"); // space
|
|
expect(url).toContain("%26"); // &
|
|
});
|
|
|
|
it("should not include response-content-disposition when empty string is provided", () => {
|
|
const file = s3Client.file("test.txt");
|
|
|
|
const url = file.presign({
|
|
method: "GET",
|
|
contentDisposition: "",
|
|
});
|
|
|
|
expect(url).not.toContain("response-content-disposition=");
|
|
});
|
|
|
|
it("should not include response-content-type when empty string is provided", () => {
|
|
const file = s3Client.file("test.txt");
|
|
|
|
const url = file.presign({
|
|
method: "GET",
|
|
type: "",
|
|
});
|
|
|
|
expect(url).not.toContain("response-content-type=");
|
|
});
|
|
|
|
it("query parameters should be in correct alphabetical order", () => {
|
|
const file = s3Client.file("test.txt");
|
|
|
|
const url = file.presign({
|
|
method: "GET",
|
|
contentDisposition: "inline",
|
|
type: "text/plain",
|
|
});
|
|
|
|
// Check that response-content-disposition comes before response-content-type
|
|
// and both come after X-Amz-SignedHeaders and before any x-amz-* lowercase params
|
|
const dispositionIndex = url.indexOf("response-content-disposition=");
|
|
const typeIndex = url.indexOf("response-content-type=");
|
|
const signedHeadersIndex = url.indexOf("X-Amz-SignedHeaders=");
|
|
|
|
expect(dispositionIndex).toBeGreaterThan(signedHeadersIndex);
|
|
expect(typeIndex).toBeGreaterThan(dispositionIndex);
|
|
});
|
|
});
|