Skip to content

docs: say that new Response(S3File) has no body - #46874

Open
robobun wants to merge 1 commit into
mainfrom
robobun/69a0f272/docs-s3-response-no-body
Open

robobun wants to merge 1 commit into
mainfrom
robobun/69a0f272/docs-s3-response-no-body

Conversation

@robobun

@robobun robobun commented Oct 25, 2026 •

Copy link
Copy Markdown
Collaborator

Problem

  • docs/runtime/s3.mdx says that new Response(s3file) is a redirect. It does not say that the response has no body.
  • So await dest.write(new Response(s3file)) and await Bun.write(path, new Response(s3file)) read like a copy. Each one resolves with 0 and leaves an empty destination, also when the destination had content. The page has no example of a copy.

Fix

  • The new Response(S3File) section now says that the response has no body, what response.text() and a write of the response give, and what to pass instead.
  • One example shows the two replacements: write(s3file) to copy the file, and new Response(s3file.stream()) for a response that carries its bytes.
  • Verified: each new sentence and the example, as written, ran against the S3 server in test/packages/s3-server, on 1.4.3-canary.1 and on a debug build of main (179664e). The script and its output are in Notes.

Background

  • new Response(s3file) is a 302 with a presigned URL in Location. Bun.serve sends it so that the server does not download the file (feat(s3) s3 client  #15740). Its body is null (Keep body stream bookkeeping independent of the body's source #42116).
  • Bun.write() and S3File.write() write the body of the Response they get. A null body gives an empty file, as for Response.redirect(url).
  • This PR changes no code. A write() that rejects this response, or that reads the S3 file, was weighed and left out. The same question applies to every consumer of the body (text(), .body, Bun.spawn stdin), and the answer is a decision for a maintainer.
Notes

Where this comes from. No user reported it. A test run of another S3 change found that write(new Response(s3file)) stores an empty object. A check of the history showed that the empty body is deliberate, so the docs are the place that was missing it.

The script. Run it from the repo root. It needs no network.

import { S3Client } from "bun";
import { serve } from "./test/packages/s3-server/index.ts";
import { tmpdir } from "os";
import { join } from "path";
const server = serve({ buckets: ["my-bucket"] });
const size = key => server.buckets.get("my-bucket").current(key)?.data.size;
const client = new S3Client(server.clientOptions("my-bucket"));
const s3file = client.file("123.json");
await s3file.write(JSON.stringify({ name: "John", age: 30 }));

{
  // the first example of the section
  const response = new Response(s3file);
  console.log("status:", response.status, "| body:", response.body, "| text:", JSON.stringify(await response.text()));
}
{
  // "Bun writes an empty file when you pass the response to Bun.write() or to the write() method of an S3File."
  const path = join(tmpdir(), `s3-docs-${process.pid}.json`);
  await Bun.write(path, "old content");
  console.log("Bun.write(path, response):", await Bun.write(path, new Response(s3file)), "| file size:", Bun.file(path).size);
  await Bun.file(path).unlink();
  console.log("Bun.write(s3file, response):", await Bun.write(client.file("a.json"), new Response(s3file)), "| object size:", size("a.json"));
  console.log("s3file.write(response):", await client.file("b.json").write(new Response(s3file)), "| object size:", size("b.json"));
}
{
  // the new example, as written
  // Bun streams the S3 file to the destination
  await client.file("copy.json").write(s3file);

  // A 200 response with the bytes of the S3 file as its body
  const response = new Response(s3file.stream());

  console.log("copy.json:", await client.file("copy.json").text(), "| object size:", size("copy.json"));
  console.log("response:", response.status, await response.text());
}
console.log("requests:", server.requests.map(r => `${r.operation}:${r.key}`).join(" "));
server.stop(true);

The output. It is the same on 1.4.3-canary.1+367d939d9 and on a debug build of main (179664e).

status: 302 | body: null | text: ""
Bun.write(path, response): 0 | file size: 0
Bun.write(s3file, response): 0 | object size: 0
s3file.write(response): 0 | object size: 0
copy.json: {"name":"John","age":30} | object size: 24
response: 200 {"name":"John","age":30}
requests: PutObject:123.json PutObject:a.json PutObject:b.json GetObject:123.json PutObject:copy.json GetObject:123.json GetObject:copy.json

The request log shows the cause of the empty destination. The two writes of the redirect response send a PutObject for a.json and b.json and no GetObject for 123.json.

Why the body is empty. The S3 branch of the Response constructor (src/runtime/webcore/Response.rs:1018) signs a GET URL, sets status 302 and Location, and stores a null body. It keeps no reference to the S3File. #15740 shipped it with an empty body, and #42116 made that body null, as for Response.redirect(). write_file_internal (src/runtime/webcore/Blob.rs:4567) then handles the response like new Response(null).

Other forms, for comparison. write(s3file), write(new Response(s3file.stream())) and write(new Request(url, { method: "POST", body: s3file })) store all the bytes. A Request keeps the S3File as its body. Only the Response constructor makes a redirect from it.

Not in this PR.


no test proof · iteration 0 · docs-only change; test-proof not applicable

The `new Response(S3File)` section of the S3 page says that the response
is a redirect. It does not say what a read or a write of that response
gives.

The section now says that the response has no body, that `Bun.write()`
and `S3File.write()` write an empty file from it, and what to pass
instead: the `S3File` to copy the file, or `s3file.stream()` for a
response that carries its bytes.
@robobun

robobun commented Oct 25, 2026 •

Copy link
Copy Markdown
Collaborator Author
Updated 5:15 AM PT - Oct 25th, 2026

✅ @robobun, your commit d72fa05eb8c91a61e5a9ee05f09900839ca54953 passed in Build #132577! 🎉


🧪   To try this PR locally:

bunx bun-pr 46874

That installs a local version of the PR into your bun-46874 executable, so you can run:

bun-46874 --bun

@robobun

robobun commented Oct 25, 2026

Copy link
Copy Markdown
Collaborator Author

Status

Reproduced on 1.4.3-canary.1 and on a debug build of main (179664e). Run this from the repo root. It uses the S3 server in test/packages/s3-server and no network.

import { S3Client } from "bun";
import { serve } from "./test/packages/s3-server/index.ts";
const server = serve({ buckets: ["bucket"] });
const client = new S3Client(server.clientOptions("bucket"));
await client.write("from", "hello world");
const size = key => server.buckets.get("bucket").current(key)?.data.size;
console.log(await client.file("a").write(new Response(client.file("from"))), size("a")); // 0 0
console.log(await client.file("d").write(client.file("from")), size("d")); // 11 11
server.stop(true);

new Response(s3file) is the documented redirect and has no body, so the first write stores an empty object. This PR adds that fact to the docs and shows how to copy the file. It changes no code.

PR: #46874

@coderabbitai

coderabbitai Bot commented Oct 25, 2026

Copy link
Copy Markdown
Contributor

Review in Change Stack →

No actionable comments were generated in the recent review.

Recent review info
Run configuration
Commits

Reviewing files that changed from the base of the PR and between 179664e and d72fa05.

Files selected for processing: 1
  • docs/runtime/s3.mdx

Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 4 remain after this review.

Walkthrough

The S3 runtime documentation now explains that new Response(S3File) has an empty body. It shows how to copy an S3File directly and how to create a response containing the file’s bytes by passing its stream.

Changes

S3 response documentation

Layer / File(s) Summary
Document response body behavior
docs/runtime/s3.mdx
The documentation states that a response constructed from an S3File has an empty body. It shows copying the file directly and creating a response with s3file.stream().

Possibly related PRs

  • oven-sh/bun#38364: Addresses empty FormData parts for S3-backed entries and recommends materializing the bytes.
  • oven-sh/bun#42786: Addresses empty stdin when an S3-backed Response is used as shell input.
  • oven-sh/bun#46673: Defines behavior for APIs that consume bytes from an S3-backed Blob.

Suggested reviewers: alii

Priority: Normal

Merge Risk: Minimal · up to d72fa

No actionable merge-blocking risk remains.

Pre-merge checks: Passed 4
Check name Status Explanation
Title check Passed The title "docs: say that new Response(S3File) has no body" is a short, specific, single-sentence summary of the main change. The PR updates docs/runtime/s3.mdx to state that the redirect response f…
Description check Passed The description follows the template's two sections. 'How did you verify your code works?' is answered in the Fix section, which says each new sentence and the example were run against the S3 server i…
Linked Issues check Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check Passed Check skipped because no linked issues were found for this pull request.
Full details: Title check

Explanation

The title "docs: say that new Response(S3File) has no body" is a short, specific, single-sentence summary of the main change. The PR updates docs/runtime/s3.mdx to state that the redirect response from new Response(S3File) has no body, which the title captures directly. It is a conventional docs: prefix title and does not contain file lists, emojis, or vague terms.

Full details: Description check

Explanation

The description follows the template's two sections. 'How did you verify your code works?' is answered in the Fix section, which says each new sentence and the example were run against the S3 server in test/packages/s3-server on 1.4.3-canary.1 and a debug build of main. The full script and output are in Notes. 'What does this PR do?' is covered by the Problem and Fix sections, which explain the missing documentation and the added copy example. The description also gives background on the cause and scope, so it is complete for a docs-only change.

  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code review found no issues

No high-confidence issues detected in this change.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant