Compare commits

...

2 Commits

Author SHA1 Message Date
Claude Bot
46ee6da96d test(s3): add symmetric test for static list() with requestPayer: false
Adds a test to verify the x-amz-request-payer header is NOT included
when calling S3Client.list() statically with requestPayer: false.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-06 14:37:07 +00:00
Claude Bot
91a22fa674 fix(s3): add requestPayer support to list() method
`S3Client.list()` did not respect the `requestPayer` option, causing
`AccessDenied` errors on Requester Pays buckets. This was because the
`listObjects()` function in the S3 client was missing the `request_payer`
parameter that other operations (like `delete()`) already had.

Changes:
- Add `request_payer` parameter to `listObjects()` in `src/s3/client.zig`
- Pass `request_payer` to `signRequest()` to include the
  `x-amz-request-payer: requester` header
- Update Store.zig to pass `request_payer` when calling `listObjects()`
- Add `requestPayer` to the TypeScript types for `list()` options

Fixes #26778

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-06 14:24:00 +00:00
4 changed files with 138 additions and 3 deletions

View File

@@ -1284,7 +1284,10 @@ declare module "bun" {
*/
list(
input?: S3ListObjectsOptions | null,
options?: Pick<S3Options, "accessKeyId" | "secretAccessKey" | "sessionToken" | "region" | "bucket" | "endpoint">,
options?: Pick<
S3Options,
"accessKeyId" | "secretAccessKey" | "sessionToken" | "region" | "bucket" | "endpoint" | "requestPayer"
>,
): Promise<S3ListObjectsResponse>;
/**
@@ -1320,7 +1323,10 @@ declare module "bun" {
*/
static list(
input?: S3ListObjectsOptions | null,
options?: Pick<S3Options, "accessKeyId" | "secretAccessKey" | "sessionToken" | "region" | "bucket" | "endpoint">,
options?: Pick<
S3Options,
"accessKeyId" | "secretAccessKey" | "sessionToken" | "region" | "bucket" | "endpoint" | "requestPayer"
>,
): Promise<S3ListObjectsResponse>;
}

View File

@@ -427,7 +427,7 @@ pub const S3 = struct {
.store = store, // store is needed in case of not found error
.resolvedlistOptions = options,
.global = globalThis,
}), proxy);
}), proxy, aws_options.request_payer);
return value;
}

View File

@@ -109,6 +109,7 @@ pub fn listObjects(
callback: *const fn (S3ListObjectsResult, *anyopaque) bun.JSTerminated!void,
callback_context: *anyopaque,
proxy_url: ?[]const u8,
request_payer: bool,
) bun.JSTerminated!void {
var search_params: bun.ByteList = .{};
@@ -177,6 +178,7 @@ pub fn listObjects(
.path = "",
.method = .GET,
.search_params = search_params.slice(),
.request_payer = request_payer,
}, true, null) catch |sign_err| {
search_params.deinit(bun.default_allocator);

View File

@@ -248,4 +248,131 @@ describe("s3 - Requester Pays", () => {
expect(url.searchParams.get("x-amz-request-payer")).toBeNull();
});
it("should include x-amz-request-payer header in list() requests when requestPayer is true", async () => {
let reqHeaders: Headers | undefined = undefined;
let reqMethod: string | undefined = undefined;
using server = Bun.serve({
port: 0,
async fetch(req) {
reqHeaders = req.headers;
reqMethod = req.method;
return new Response(
`<?xml version="1.0" encoding="UTF-8"?><ListBucketResult xmlns="http://s3.amazonaws.com/doc/2006-03-01/">
<Name>my_bucket</Name>
</ListBucketResult>`,
{
headers: {
"Content-Type": "application/xml",
},
status: 200,
},
);
},
});
const client = new S3Client({
...s3Options,
endpoint: server.url.href,
requestPayer: true,
});
await client.list({ prefix: "test/" });
expect(reqMethod).toBe("GET");
expect(reqHeaders!.get("authorization")).toInclude("x-amz-request-payer");
expect(reqHeaders!.get("x-amz-request-payer")).toBe("requester");
});
it("should NOT include x-amz-request-payer header in list() requests when requestPayer is false", async () => {
let reqHeaders: Headers | undefined = undefined;
using server = Bun.serve({
port: 0,
async fetch(req) {
reqHeaders = req.headers;
return new Response(
`<?xml version="1.0" encoding="UTF-8"?><ListBucketResult xmlns="http://s3.amazonaws.com/doc/2006-03-01/">
<Name>my_bucket</Name>
</ListBucketResult>`,
{
headers: {
"Content-Type": "application/xml",
},
status: 200,
},
);
},
});
const client = new S3Client({
...s3Options,
endpoint: server.url.href,
requestPayer: false,
});
await client.list();
expect(reqHeaders!.get("authorization")).not.toInclude("x-amz-request-payer");
expect(reqHeaders!.get("x-amz-request-payer")).toBeNull();
});
it("should include x-amz-request-payer header in static list() when requestPayer is true", async () => {
let reqHeaders: Headers | undefined = undefined;
using server = Bun.serve({
port: 0,
async fetch(req) {
reqHeaders = req.headers;
return new Response(
`<?xml version="1.0" encoding="UTF-8"?><ListBucketResult xmlns="http://s3.amazonaws.com/doc/2006-03-01/">
<Name>my_bucket</Name>
</ListBucketResult>`,
{
headers: {
"Content-Type": "application/xml",
},
status: 200,
},
);
},
});
await S3Client.list(null, {
...s3Options,
endpoint: server.url.href,
requestPayer: true,
});
expect(reqHeaders!.get("authorization")).toInclude("x-amz-request-payer");
expect(reqHeaders!.get("x-amz-request-payer")).toBe("requester");
});
it("should NOT include x-amz-request-payer header in static list() when requestPayer is false", async () => {
let reqHeaders: Headers | undefined = undefined;
using server = Bun.serve({
port: 0,
async fetch(req) {
reqHeaders = req.headers;
return new Response(
`<?xml version="1.0" encoding="UTF-8"?><ListBucketResult xmlns="http://s3.amazonaws.com/doc/2006-03-01/">
<Name>my_bucket</Name>
</ListBucketResult>`,
{
headers: {
"Content-Type": "application/xml",
},
status: 200,
},
);
},
});
await S3Client.list(null, {
...s3Options,
endpoint: server.url.href,
requestPayer: false,
});
expect(reqHeaders!.get("authorization")).not.toInclude("x-amz-request-payer");
expect(reqHeaders!.get("x-amz-request-payer")).toBeNull();
});
});