mirror of
https://github.com/oven-sh/bun
synced 2026-02-02 15:08:46 +00:00
feat(s3): add response override options for presigned URLs
Adds support for S3 presigned URL response override parameters: - responseCacheControl - responseContentDisposition - responseContentEncoding - responseContentLanguage - responseContentType - responseExpires These parameters allow setting response headers when accessing presigned URLs, similar to AWS SDK's GetObjectCommand options. Fixes #18016 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
66
packages/bun-types/s3.d.ts
vendored
66
packages/bun-types/s3.d.ts
vendored
@@ -393,6 +393,72 @@ declare module "bun" {
|
||||
* });
|
||||
*/
|
||||
method?: "GET" | "POST" | "PUT" | "DELETE" | "HEAD";
|
||||
|
||||
/**
|
||||
* Sets the Cache-Control header of the response when the presigned URL is accessed.
|
||||
*
|
||||
* @example
|
||||
* const url = file.presign({
|
||||
* expiresIn: 3600,
|
||||
* responseCacheControl: "max-age=3600, public"
|
||||
* });
|
||||
*/
|
||||
responseCacheControl?: string;
|
||||
|
||||
/**
|
||||
* Sets the Content-Disposition header of the response when the presigned URL is accessed.
|
||||
*
|
||||
* @example
|
||||
* const url = file.presign({
|
||||
* expiresIn: 3600,
|
||||
* responseContentDisposition: "attachment; filename=\"report.pdf\""
|
||||
* });
|
||||
*/
|
||||
responseContentDisposition?: string;
|
||||
|
||||
/**
|
||||
* Sets the Content-Encoding header of the response when the presigned URL is accessed.
|
||||
*
|
||||
* @example
|
||||
* const url = file.presign({
|
||||
* expiresIn: 3600,
|
||||
* responseContentEncoding: "gzip"
|
||||
* });
|
||||
*/
|
||||
responseContentEncoding?: string;
|
||||
|
||||
/**
|
||||
* Sets the Content-Language header of the response when the presigned URL is accessed.
|
||||
*
|
||||
* @example
|
||||
* const url = file.presign({
|
||||
* expiresIn: 3600,
|
||||
* responseContentLanguage: "en-US"
|
||||
* });
|
||||
*/
|
||||
responseContentLanguage?: string;
|
||||
|
||||
/**
|
||||
* Sets the Content-Type header of the response when the presigned URL is accessed.
|
||||
*
|
||||
* @example
|
||||
* const url = file.presign({
|
||||
* expiresIn: 3600,
|
||||
* responseContentType: "application/pdf"
|
||||
* });
|
||||
*/
|
||||
responseContentType?: string;
|
||||
|
||||
/**
|
||||
* Sets the Expires header of the response when the presigned URL is accessed.
|
||||
*
|
||||
* @example
|
||||
* const url = file.presign({
|
||||
* expiresIn: 3600,
|
||||
* responseExpires: "Wed, 21 Oct 2025 07:28:00 GMT"
|
||||
* });
|
||||
*/
|
||||
responseExpires?: string;
|
||||
}
|
||||
|
||||
interface S3Stats {
|
||||
|
||||
@@ -505,6 +505,12 @@ pub fn getPresignUrlFrom(this: *Blob, globalThis: *jsc.JSGlobalObject, extra_opt
|
||||
.request_payer = credentialsWithOptions.request_payer,
|
||||
.content_disposition = credentialsWithOptions.content_disposition,
|
||||
.content_type = credentialsWithOptions.content_type,
|
||||
.response_cache_control = credentialsWithOptions.response_cache_control,
|
||||
.response_content_disposition = credentialsWithOptions.response_content_disposition,
|
||||
.response_content_encoding = credentialsWithOptions.response_content_encoding,
|
||||
.response_content_language = credentialsWithOptions.response_content_language,
|
||||
.response_content_type = credentialsWithOptions.response_content_type,
|
||||
.response_expires = credentialsWithOptions.response_expires,
|
||||
}, false, .{ .expires = expires }) catch |sign_err| {
|
||||
return S3.throwSignError(sign_err, globalThis);
|
||||
};
|
||||
|
||||
@@ -262,6 +262,97 @@ pub const S3Credentials = struct {
|
||||
if (try opts.getBooleanStrict(globalObject, "requestPayer")) |request_payer| {
|
||||
new_credentials.request_payer = request_payer;
|
||||
}
|
||||
|
||||
// Response override options for presigned URLs
|
||||
if (try opts.getTruthyComptime(globalObject, "responseCacheControl")) |js_value| {
|
||||
if (!js_value.isEmptyOrUndefinedOrNull()) {
|
||||
if (js_value.isString()) {
|
||||
const str = try bun.String.fromJS(js_value, globalObject);
|
||||
defer str.deref();
|
||||
if (str.tag != .Empty and str.tag != .Dead) {
|
||||
new_credentials._responseCacheControlSlice = str.toUTF8(bun.default_allocator);
|
||||
new_credentials.response_cache_control = new_credentials._responseCacheControlSlice.?.slice();
|
||||
}
|
||||
} else {
|
||||
return globalObject.throwInvalidArgumentTypeValue("responseCacheControl", "string", js_value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (try opts.getTruthyComptime(globalObject, "responseContentDisposition")) |js_value| {
|
||||
if (!js_value.isEmptyOrUndefinedOrNull()) {
|
||||
if (js_value.isString()) {
|
||||
const str = try bun.String.fromJS(js_value, globalObject);
|
||||
defer str.deref();
|
||||
if (str.tag != .Empty and str.tag != .Dead) {
|
||||
new_credentials._responseContentDispositionSlice = str.toUTF8(bun.default_allocator);
|
||||
new_credentials.response_content_disposition = new_credentials._responseContentDispositionSlice.?.slice();
|
||||
}
|
||||
} else {
|
||||
return globalObject.throwInvalidArgumentTypeValue("responseContentDisposition", "string", js_value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (try opts.getTruthyComptime(globalObject, "responseContentEncoding")) |js_value| {
|
||||
if (!js_value.isEmptyOrUndefinedOrNull()) {
|
||||
if (js_value.isString()) {
|
||||
const str = try bun.String.fromJS(js_value, globalObject);
|
||||
defer str.deref();
|
||||
if (str.tag != .Empty and str.tag != .Dead) {
|
||||
new_credentials._responseContentEncodingSlice = str.toUTF8(bun.default_allocator);
|
||||
new_credentials.response_content_encoding = new_credentials._responseContentEncodingSlice.?.slice();
|
||||
}
|
||||
} else {
|
||||
return globalObject.throwInvalidArgumentTypeValue("responseContentEncoding", "string", js_value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (try opts.getTruthyComptime(globalObject, "responseContentLanguage")) |js_value| {
|
||||
if (!js_value.isEmptyOrUndefinedOrNull()) {
|
||||
if (js_value.isString()) {
|
||||
const str = try bun.String.fromJS(js_value, globalObject);
|
||||
defer str.deref();
|
||||
if (str.tag != .Empty and str.tag != .Dead) {
|
||||
new_credentials._responseContentLanguageSlice = str.toUTF8(bun.default_allocator);
|
||||
new_credentials.response_content_language = new_credentials._responseContentLanguageSlice.?.slice();
|
||||
}
|
||||
} else {
|
||||
return globalObject.throwInvalidArgumentTypeValue("responseContentLanguage", "string", js_value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (try opts.getTruthyComptime(globalObject, "responseContentType")) |js_value| {
|
||||
if (!js_value.isEmptyOrUndefinedOrNull()) {
|
||||
if (js_value.isString()) {
|
||||
const str = try bun.String.fromJS(js_value, globalObject);
|
||||
defer str.deref();
|
||||
if (str.tag != .Empty and str.tag != .Dead) {
|
||||
new_credentials._responseContentTypeSlice = str.toUTF8(bun.default_allocator);
|
||||
new_credentials.response_content_type = new_credentials._responseContentTypeSlice.?.slice();
|
||||
}
|
||||
} else {
|
||||
return globalObject.throwInvalidArgumentTypeValue("responseContentType", "string", js_value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (try opts.getTruthyComptime(globalObject, "responseExpires")) |js_value| {
|
||||
if (!js_value.isEmptyOrUndefinedOrNull()) {
|
||||
if (js_value.isString()) {
|
||||
const str = try bun.String.fromJS(js_value, globalObject);
|
||||
defer str.deref();
|
||||
if (str.tag != .Empty and str.tag != .Dead) {
|
||||
new_credentials._responseExpiresSlice = str.toUTF8(bun.default_allocator);
|
||||
new_credentials.response_expires = new_credentials._responseExpiresSlice.?.slice();
|
||||
}
|
||||
} else {
|
||||
return globalObject.throwInvalidArgumentTypeValue("responseExpires", "string", js_value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return new_credentials;
|
||||
@@ -465,6 +556,13 @@ pub const S3Credentials = struct {
|
||||
acl: ?ACL = null,
|
||||
storage_class: ?StorageClass = null,
|
||||
request_payer: bool = false,
|
||||
// Response override options for presigned URLs
|
||||
response_cache_control: ?[]const u8 = null,
|
||||
response_content_disposition: ?[]const u8 = null,
|
||||
response_content_encoding: ?[]const u8 = null,
|
||||
response_content_language: ?[]const u8 = null,
|
||||
response_content_type: ?[]const u8 = null,
|
||||
response_expires: ?[]const u8 = null,
|
||||
};
|
||||
/// This is not used for signing but for console.log output, is just nice to have
|
||||
pub fn guessBucket(endpoint: []const u8) ?[]const u8 {
|
||||
@@ -750,13 +848,50 @@ pub const S3Credentials = struct {
|
||||
encoded_content_type = encodeURIComponent(ct, &content_type_encoded_buffer, true) catch return error.FailedToGenerateSignature;
|
||||
}
|
||||
|
||||
// Additional response override parameters
|
||||
var response_cache_control_encoded_buffer: [256]u8 = undefined;
|
||||
var encoded_response_cache_control: ?[]const u8 = null;
|
||||
if (signOptions.response_cache_control) |rcc| {
|
||||
encoded_response_cache_control = encodeURIComponent(rcc, &response_cache_control_encoded_buffer, true) catch return error.FailedToGenerateSignature;
|
||||
}
|
||||
|
||||
var response_content_disposition_encoded_buffer: [512]u8 = undefined;
|
||||
var encoded_response_content_disposition: ?[]const u8 = null;
|
||||
if (signOptions.response_content_disposition) |rcd| {
|
||||
encoded_response_content_disposition = encodeURIComponent(rcd, &response_content_disposition_encoded_buffer, true) catch return error.FailedToGenerateSignature;
|
||||
}
|
||||
|
||||
var response_content_encoding_encoded_buffer: [256]u8 = undefined;
|
||||
var encoded_response_content_encoding: ?[]const u8 = null;
|
||||
if (signOptions.response_content_encoding) |rce| {
|
||||
encoded_response_content_encoding = encodeURIComponent(rce, &response_content_encoding_encoded_buffer, true) catch return error.FailedToGenerateSignature;
|
||||
}
|
||||
|
||||
var response_content_language_encoded_buffer: [256]u8 = undefined;
|
||||
var encoded_response_content_language: ?[]const u8 = null;
|
||||
if (signOptions.response_content_language) |rcl| {
|
||||
encoded_response_content_language = encodeURIComponent(rcl, &response_content_language_encoded_buffer, true) catch return error.FailedToGenerateSignature;
|
||||
}
|
||||
|
||||
var response_content_type_encoded_buffer: [256]u8 = undefined;
|
||||
var encoded_response_content_type: ?[]const u8 = null;
|
||||
if (signOptions.response_content_type) |rct| {
|
||||
encoded_response_content_type = encodeURIComponent(rct, &response_content_type_encoded_buffer, true) catch return error.FailedToGenerateSignature;
|
||||
}
|
||||
|
||||
var response_expires_encoded_buffer: [256]u8 = undefined;
|
||||
var encoded_response_expires: ?[]const u8 = null;
|
||||
if (signOptions.response_expires) |re| {
|
||||
encoded_response_expires = encodeURIComponent(re, &response_expires_encoded_buffer, true) catch return error.FailedToGenerateSignature;
|
||||
}
|
||||
|
||||
// Build query parameters in alphabetical order for AWS Signature V4 canonical request
|
||||
const canonical = brk_canonical: {
|
||||
var stack_fallback = std.heap.stackFallback(512, bun.default_allocator);
|
||||
const allocator = stack_fallback.get();
|
||||
var query_parts: bun.BoundedArray([]const u8, 13) = .{};
|
||||
var query_parts: bun.BoundedArray([]const u8, 19) = .{};
|
||||
|
||||
// Add parameters in alphabetical order: Content-MD5, X-Amz-Acl, X-Amz-Algorithm, X-Amz-Credential, X-Amz-Date, X-Amz-Expires, X-Amz-Security-Token, X-Amz-SignedHeaders, response-content-disposition, response-content-type, x-amz-request-payer, x-amz-storage-class
|
||||
// Add parameters in alphabetical order for AWS Signature V4 canonical request
|
||||
|
||||
if (encoded_content_md5) |encoded_content_md5_value| {
|
||||
try query_parts.append(try std.fmt.allocPrint(allocator, "Content-MD5={s}", .{encoded_content_md5_value}));
|
||||
@@ -780,14 +915,37 @@ pub const S3Credentials = struct {
|
||||
|
||||
try query_parts.append(try std.fmt.allocPrint(allocator, "X-Amz-SignedHeaders=host", .{}));
|
||||
|
||||
if (encoded_content_disposition) |cd| {
|
||||
// Response override parameters (in alphabetical order)
|
||||
if (encoded_response_cache_control) |rcc| {
|
||||
try query_parts.append(try std.fmt.allocPrint(allocator, "response-cache-control={s}", .{rcc}));
|
||||
}
|
||||
|
||||
if (encoded_response_content_disposition) |rcd| {
|
||||
try query_parts.append(try std.fmt.allocPrint(allocator, "response-content-disposition={s}", .{rcd}));
|
||||
} else if (encoded_content_disposition) |cd| {
|
||||
// Fall back to content_disposition for backwards compatibility
|
||||
try query_parts.append(try std.fmt.allocPrint(allocator, "response-content-disposition={s}", .{cd}));
|
||||
}
|
||||
|
||||
if (encoded_content_type) |ct| {
|
||||
if (encoded_response_content_encoding) |rce| {
|
||||
try query_parts.append(try std.fmt.allocPrint(allocator, "response-content-encoding={s}", .{rce}));
|
||||
}
|
||||
|
||||
if (encoded_response_content_language) |rcl| {
|
||||
try query_parts.append(try std.fmt.allocPrint(allocator, "response-content-language={s}", .{rcl}));
|
||||
}
|
||||
|
||||
if (encoded_response_content_type) |rct| {
|
||||
try query_parts.append(try std.fmt.allocPrint(allocator, "response-content-type={s}", .{rct}));
|
||||
} else if (encoded_content_type) |ct| {
|
||||
// Fall back to content_type for backwards compatibility
|
||||
try query_parts.append(try std.fmt.allocPrint(allocator, "response-content-type={s}", .{ct}));
|
||||
}
|
||||
|
||||
if (encoded_response_expires) |re| {
|
||||
try query_parts.append(try std.fmt.allocPrint(allocator, "response-expires={s}", .{re}));
|
||||
}
|
||||
|
||||
if (request_payer) {
|
||||
try query_parts.append(try std.fmt.allocPrint(allocator, "x-amz-request-payer=requester", .{}));
|
||||
}
|
||||
@@ -817,9 +975,9 @@ pub const S3Credentials = struct {
|
||||
// Build final URL with query parameters in alphabetical order to match canonical request
|
||||
var url_stack_fallback = std.heap.stackFallback(512, bun.default_allocator);
|
||||
const url_allocator = url_stack_fallback.get();
|
||||
var url_query_parts: bun.BoundedArray([]const u8, 14) = .{};
|
||||
var url_query_parts: bun.BoundedArray([]const u8, 20) = .{};
|
||||
|
||||
// Add parameters in alphabetical order: Content-MD5, X-Amz-Acl, X-Amz-Algorithm, X-Amz-Credential, X-Amz-Date, X-Amz-Expires, X-Amz-Security-Token, X-Amz-Signature, X-Amz-SignedHeaders, response-content-disposition, response-content-type, x-amz-request-payer, x-amz-storage-class
|
||||
// Add parameters in alphabetical order for AWS Signature V4
|
||||
|
||||
if (encoded_content_md5) |encoded_content_md5_value| {
|
||||
try url_query_parts.append(try std.fmt.allocPrint(url_allocator, "Content-MD5={s}", .{encoded_content_md5_value}));
|
||||
@@ -845,14 +1003,37 @@ pub const S3Credentials = struct {
|
||||
|
||||
try url_query_parts.append(try std.fmt.allocPrint(url_allocator, "X-Amz-SignedHeaders=host", .{}));
|
||||
|
||||
if (encoded_content_disposition) |cd| {
|
||||
// Response override parameters (in alphabetical order)
|
||||
if (encoded_response_cache_control) |rcc| {
|
||||
try url_query_parts.append(try std.fmt.allocPrint(url_allocator, "response-cache-control={s}", .{rcc}));
|
||||
}
|
||||
|
||||
if (encoded_response_content_disposition) |rcd| {
|
||||
try url_query_parts.append(try std.fmt.allocPrint(url_allocator, "response-content-disposition={s}", .{rcd}));
|
||||
} else if (encoded_content_disposition) |cd| {
|
||||
// Fall back to content_disposition for backwards compatibility
|
||||
try url_query_parts.append(try std.fmt.allocPrint(url_allocator, "response-content-disposition={s}", .{cd}));
|
||||
}
|
||||
|
||||
if (encoded_content_type) |ct| {
|
||||
if (encoded_response_content_encoding) |rce| {
|
||||
try url_query_parts.append(try std.fmt.allocPrint(url_allocator, "response-content-encoding={s}", .{rce}));
|
||||
}
|
||||
|
||||
if (encoded_response_content_language) |rcl| {
|
||||
try url_query_parts.append(try std.fmt.allocPrint(url_allocator, "response-content-language={s}", .{rcl}));
|
||||
}
|
||||
|
||||
if (encoded_response_content_type) |rct| {
|
||||
try url_query_parts.append(try std.fmt.allocPrint(url_allocator, "response-content-type={s}", .{rct}));
|
||||
} else if (encoded_content_type) |ct| {
|
||||
// Fall back to content_type for backwards compatibility
|
||||
try url_query_parts.append(try std.fmt.allocPrint(url_allocator, "response-content-type={s}", .{ct}));
|
||||
}
|
||||
|
||||
if (encoded_response_expires) |re| {
|
||||
try url_query_parts.append(try std.fmt.allocPrint(url_allocator, "response-expires={s}", .{re}));
|
||||
}
|
||||
|
||||
if (request_payer) {
|
||||
try url_query_parts.append(try std.fmt.allocPrint(url_allocator, "x-amz-request-payer=requester", .{}));
|
||||
}
|
||||
@@ -1003,6 +1184,14 @@ pub const S3CredentialsWithOptions = struct {
|
||||
changed_credentials: bool = false,
|
||||
/// indicates if the virtual hosted style is used
|
||||
virtual_hosted_style: bool = false,
|
||||
// Response override options for presigned URLs (S3 GetObject response headers)
|
||||
response_cache_control: ?[]const u8 = null,
|
||||
response_content_disposition: ?[]const u8 = null,
|
||||
response_content_encoding: ?[]const u8 = null,
|
||||
response_content_language: ?[]const u8 = null,
|
||||
response_content_type: ?[]const u8 = null,
|
||||
response_expires: ?[]const u8 = null,
|
||||
|
||||
_accessKeyIdSlice: ?jsc.ZigString.Slice = null,
|
||||
_secretAccessKeySlice: ?jsc.ZigString.Slice = null,
|
||||
_regionSlice: ?jsc.ZigString.Slice = null,
|
||||
@@ -1012,6 +1201,12 @@ pub const S3CredentialsWithOptions = struct {
|
||||
_contentDispositionSlice: ?jsc.ZigString.Slice = null,
|
||||
_contentTypeSlice: ?jsc.ZigString.Slice = null,
|
||||
_contentEncodingSlice: ?jsc.ZigString.Slice = null,
|
||||
_responseCacheControlSlice: ?jsc.ZigString.Slice = null,
|
||||
_responseContentDispositionSlice: ?jsc.ZigString.Slice = null,
|
||||
_responseContentEncodingSlice: ?jsc.ZigString.Slice = null,
|
||||
_responseContentLanguageSlice: ?jsc.ZigString.Slice = null,
|
||||
_responseContentTypeSlice: ?jsc.ZigString.Slice = null,
|
||||
_responseExpiresSlice: ?jsc.ZigString.Slice = null,
|
||||
|
||||
pub fn deinit(this: *@This()) void {
|
||||
if (this._accessKeyIdSlice) |slice| slice.deinit();
|
||||
@@ -1023,6 +1218,12 @@ pub const S3CredentialsWithOptions = struct {
|
||||
if (this._contentDispositionSlice) |slice| slice.deinit();
|
||||
if (this._contentTypeSlice) |slice| slice.deinit();
|
||||
if (this._contentEncodingSlice) |slice| slice.deinit();
|
||||
if (this._responseCacheControlSlice) |slice| slice.deinit();
|
||||
if (this._responseContentDispositionSlice) |slice| slice.deinit();
|
||||
if (this._responseContentEncodingSlice) |slice| slice.deinit();
|
||||
if (this._responseContentLanguageSlice) |slice| slice.deinit();
|
||||
if (this._responseContentTypeSlice) |slice| slice.deinit();
|
||||
if (this._responseExpiresSlice) |slice| slice.deinit();
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
149
test/regression/issue/18016.test.ts
Normal file
149
test/regression/issue/18016.test.ts
Normal file
@@ -0,0 +1,149 @@
|
||||
import { S3Client } from "bun";
|
||||
import { describe, expect, test } from "bun:test";
|
||||
|
||||
describe("S3 presign response override options (#18016)", () => {
|
||||
const s3 = new S3Client({
|
||||
accessKeyId: "test-key",
|
||||
secretAccessKey: "test-secret",
|
||||
endpoint: "https://s3.example.com",
|
||||
bucket: "test-bucket",
|
||||
});
|
||||
|
||||
test("presign should support responseCacheControl option", () => {
|
||||
const url = s3.presign("test-file.txt", {
|
||||
expiresIn: 300,
|
||||
responseCacheControl: "max-age=3600, public",
|
||||
});
|
||||
|
||||
const urlObj = new URL(url);
|
||||
|
||||
// Verify response-cache-control parameter is present
|
||||
expect(urlObj.searchParams.get("response-cache-control")).toBe("max-age=3600, public");
|
||||
});
|
||||
|
||||
test("presign should support responseContentDisposition option", () => {
|
||||
const url = s3.presign("test-file.txt", {
|
||||
expiresIn: 300,
|
||||
responseContentDisposition: 'attachment; filename="report.pdf"',
|
||||
});
|
||||
|
||||
const urlObj = new URL(url);
|
||||
|
||||
// Verify response-content-disposition parameter is present
|
||||
expect(urlObj.searchParams.get("response-content-disposition")).toBe('attachment; filename="report.pdf"');
|
||||
});
|
||||
|
||||
test("presign should support responseContentEncoding option", () => {
|
||||
const url = s3.presign("test-file.txt", {
|
||||
expiresIn: 300,
|
||||
responseContentEncoding: "gzip",
|
||||
});
|
||||
|
||||
const urlObj = new URL(url);
|
||||
|
||||
// Verify response-content-encoding parameter is present
|
||||
expect(urlObj.searchParams.get("response-content-encoding")).toBe("gzip");
|
||||
});
|
||||
|
||||
test("presign should support responseContentLanguage option", () => {
|
||||
const url = s3.presign("test-file.txt", {
|
||||
expiresIn: 300,
|
||||
responseContentLanguage: "en-US",
|
||||
});
|
||||
|
||||
const urlObj = new URL(url);
|
||||
|
||||
// Verify response-content-language parameter is present
|
||||
expect(urlObj.searchParams.get("response-content-language")).toBe("en-US");
|
||||
});
|
||||
|
||||
test("presign should support responseContentType option", () => {
|
||||
const url = s3.presign("test-file.txt", {
|
||||
expiresIn: 300,
|
||||
responseContentType: "application/pdf",
|
||||
});
|
||||
|
||||
const urlObj = new URL(url);
|
||||
|
||||
// Verify response-content-type parameter is present
|
||||
expect(urlObj.searchParams.get("response-content-type")).toBe("application/pdf");
|
||||
});
|
||||
|
||||
test("presign should support responseExpires option", () => {
|
||||
const url = s3.presign("test-file.txt", {
|
||||
expiresIn: 300,
|
||||
responseExpires: "Wed, 21 Oct 2025 07:28:00 GMT",
|
||||
});
|
||||
|
||||
const urlObj = new URL(url);
|
||||
|
||||
// Verify response-expires parameter is present
|
||||
expect(urlObj.searchParams.get("response-expires")).toBe("Wed, 21 Oct 2025 07:28:00 GMT");
|
||||
});
|
||||
|
||||
test("presign should support multiple response override options", () => {
|
||||
const url = s3.presign("test-file.txt", {
|
||||
expiresIn: 300,
|
||||
responseCacheControl: "max-age=3600",
|
||||
responseContentDisposition: 'inline; filename="doc.pdf"',
|
||||
responseContentType: "application/pdf",
|
||||
});
|
||||
|
||||
const urlObj = new URL(url);
|
||||
const params = Array.from(urlObj.searchParams.keys());
|
||||
|
||||
// Verify all parameters are present and in alphabetical order
|
||||
expect(params).toContain("response-cache-control");
|
||||
expect(params).toContain("response-content-disposition");
|
||||
expect(params).toContain("response-content-type");
|
||||
|
||||
// Verify values
|
||||
expect(urlObj.searchParams.get("response-cache-control")).toBe("max-age=3600");
|
||||
expect(urlObj.searchParams.get("response-content-disposition")).toBe('inline; filename="doc.pdf"');
|
||||
expect(urlObj.searchParams.get("response-content-type")).toBe("application/pdf");
|
||||
|
||||
// Verify alphabetical order
|
||||
const expected = params.slice().sort();
|
||||
expect(params).toEqual(expected);
|
||||
});
|
||||
|
||||
test("presign should prefer responseContentType over type for response override", () => {
|
||||
const url = s3.presign("test-file.txt", {
|
||||
expiresIn: 300,
|
||||
type: "text/plain",
|
||||
responseContentType: "application/json",
|
||||
});
|
||||
|
||||
const urlObj = new URL(url);
|
||||
|
||||
// responseContentType should take precedence
|
||||
expect(urlObj.searchParams.get("response-content-type")).toBe("application/json");
|
||||
});
|
||||
|
||||
test("presign should prefer responseContentDisposition over contentDisposition for response override", () => {
|
||||
const url = s3.presign("test-file.txt", {
|
||||
expiresIn: 300,
|
||||
contentDisposition: "inline",
|
||||
responseContentDisposition: "attachment",
|
||||
});
|
||||
|
||||
const urlObj = new URL(url);
|
||||
|
||||
// responseContentDisposition should take precedence
|
||||
expect(urlObj.searchParams.get("response-content-disposition")).toBe("attachment");
|
||||
});
|
||||
|
||||
test("S3File presign method should support response override options", () => {
|
||||
const file = s3.file("test-file.txt");
|
||||
const url = file.presign({
|
||||
expiresIn: 300,
|
||||
responseCacheControl: "no-cache",
|
||||
responseContentType: "image/png",
|
||||
});
|
||||
|
||||
const urlObj = new URL(url);
|
||||
|
||||
expect(urlObj.searchParams.get("response-cache-control")).toBe("no-cache");
|
||||
expect(urlObj.searchParams.get("response-content-type")).toBe("image/png");
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user