diff --git a/src/bun.js/webcore/blob/Store.zig b/src/bun.js/webcore/blob/Store.zig
index c8347b3dd0..8cbfc8ef39 100644
--- a/src/bun.js/webcore/blob/Store.zig
+++ b/src/bun.js/webcore/blob/Store.zig
@@ -311,12 +311,8 @@ pub const S3 = struct {
pub fn path(this: *@This()) []const u8 {
var path_name = bun.URL.parse(this.pathlike.slice()).s3Path();
- // normalize start and ending
- if (strings.endsWith(path_name, "/")) {
- path_name = path_name[0..path_name.len];
- } else if (strings.endsWith(path_name, "\\")) {
- path_name = path_name[0 .. path_name.len - 1];
- }
+ // For S3, we only remove leading slashes but preserve trailing slashes
+ // as they are semantically significant (e.g., for folder representations)
if (strings.startsWith(path_name, "/")) {
path_name = path_name[1..];
} else if (strings.startsWith(path_name, "\\")) {
diff --git a/test/js/bun/s3/s3-list-objects.test.ts b/test/js/bun/s3/s3-list-objects.test.ts
index bacaca32ca..5ce69c50d4 100644
--- a/test/js/bun/s3/s3-list-objects.test.ts
+++ b/test/js/bun/s3/s3-list-objects.test.ts
@@ -1166,4 +1166,83 @@ describe.skipIf(!optionsFromEnv.accessKeyId)("S3 - CI - List Objects", () => {
expect(storedFile.owner).toBeObject();
expect(storedFile.owner!.id).toBeString();
});
+
+ it("should preserve trailing slashes in S3 keys", async () => {
+ const testKeys = [
+ "test_folder/",
+ "test_folder/subfolder/",
+ "test_file_without_slash",
+ ];
+ const uploadedKeys: string[] = [];
+
+ using server = createBunServer(async req => {
+ const url = new URL(req.url);
+
+ // Handle PUT requests (write operations)
+ if (req.method === "PUT") {
+ // Extract the key from the URL path (remove leading slash)
+ const key = url.pathname.substring(1);
+ uploadedKeys.push(key);
+
+ return new Response("", {
+ headers: {
+ ETag: '"test-etag"',
+ },
+ status: 200,
+ });
+ }
+
+ // Handle GET requests (list operations)
+ if (req.method === "GET" && url.search.includes("list-type=2")) {
+ // Return the keys exactly as they were uploaded
+ const contents = uploadedKeys.map(key =>
+ `${key}0`
+ ).join("");
+
+ return new Response(
+ `
+
+ test-bucket
+ ${contents}
+ false
+ `,
+ {
+ headers: {
+ "Content-Type": "application/xml",
+ },
+ status: 200,
+ },
+ );
+ }
+
+ return new Response("Not Found", { status: 404 });
+ });
+
+ const client = new S3Client({
+ ...options,
+ endpoint: server.url.href,
+ });
+
+ // Write objects with and without trailing slashes
+ for (const key of testKeys) {
+ await client.write(key, new ArrayBuffer(0));
+ }
+
+ // List objects and verify trailing slashes are preserved
+ const listResult = await client.list({});
+
+ expect(listResult.contents).toBeArray();
+ expect(listResult.contents).toHaveLength(3);
+
+ // Verify each key is preserved exactly as written
+ const listedKeys = listResult.contents!.map((item: any) => item.key);
+ expect(listedKeys).toContain("test_folder/");
+ expect(listedKeys).toContain("test_folder/subfolder/");
+ expect(listedKeys).toContain("test_file_without_slash");
+
+ // Specifically verify trailing slashes are preserved
+ expect(listedKeys[0]).toEndWith("/");
+ expect(listedKeys[1]).toEndWith("/");
+ expect(listedKeys[2]).not.toEndWith("/");
+ });
});