Skip to content

fetch: send text/plain Content-Type for string request bodies#33677

Open
robobun wants to merge 4 commits into
mainfrom
farm/08ef360e/fetch-string-body-content-type
Open

fetch: send text/plain Content-Type for string request bodies#33677
robobun wants to merge 4 commits into
mainfrom
farm/08ef360e/fetch-string-body-content-type

Conversation

@robobun

@robobun robobun commented Jul 7, 2026

Copy link
Copy Markdown
Collaborator

Problem

fetch(url, {method: "POST", body: "<string>"}) sends the request with no Content-Type header on the wire. node/undici, deno, and browsers all send text/plain;charset=UTF-8 for the identical call, as the WHATWG fetch spec's "extract a body" step mandates for scalar-value-string bodies.

import net from "node:net";
const srv = net.createServer(s => {
  let buf = Buffer.alloc(0);
  s.on("data", d => {
    buf = Buffer.concat([buf, d]);
    if (buf.indexOf("\r\n\r\n") < 0) return;
    console.log(buf.toString("latin1").split("\r\n").find(x => /^content-type:/i.test(x)) ?? "<no Content-Type>");
    s.end("HTTP/1.1 200 OK\r\nContent-Length: 0\r\nConnection: close\r\n\r\n");
  });
});
await new Promise(r => srv.listen(0, "127.0.0.1", r));
await (await fetch(`http://127.0.0.1:${srv.address().port}/`, { method: "POST", body: "hello" })).arrayBuffer();
srv.close();

// node v26:  content-type: text/plain;charset=UTF-8
// deno 2.9:  content-type: text/plain;charset=UTF-8
// bun:       <no Content-Type>

The same omission made new Request("http://x/", {method:"POST", body:"hello"}).headers.get("content-type") and new Response("hello").headers.get("content-type") both return null.

Servers that dispatch their body parser on Content-Type (express.text(), Rails, many WAFs and API gateways) see an untyped POST from bun: bodies silently not parsed, or rejected with 415.

Cause

A string body is stored as body::Value::WTFStringImpl (or InternalBlob { was_string: true } after UTF-8 conversion). AnyBlob::content_type() already returns text/plain;charset=utf-8 for these, but every site that copies the body's type into the header list is gated one of two ways:

  • the fetch wire path gates on AnyBlob::has_content_type_from_user(), which returned false for both string variants;
  • the Request and Response constructor / lazy-headers paths match only BodyValue::Blob(_).

So the string body's implicit type was never written to the header list. URLSearchParams and FormData bodies already worked because they are stored as Value::Blob with content_type_was_set.

Fix

Add body::Value::content_type() returning the type the spec assigns to each body variant: a Blob's explicit content type if set, text/plain;charset=utf-8 for WTFStringImpl and InternalBlob { was_string: true }, and None for ArrayBuffer / ReadableStream / null bodies (which the spec assigns no type). Use it at every site that copies the body's type into the header list:

  • Request::construct_into / ensure_fetch_headers / get_content_type
  • Response::constructor / get_or_create_headers / get_content_type

Update AnyBlob::has_content_type_from_user() to return true for WTFStringImpl and InternalBlob { was_string: true } so the fetch wire serializer (from_fetch_headers via any_blob_content_type_opt) and the headers_ref helpers see string bodies as having a type. InternalBlob { was_string: false } (ArrayBuffer / typed-array bodies) keeps returning false, so those still correctly get no Content-Type.

Response.json() previously routed through get_or_create_headers() and then put_default(ContentType, json). With the change above a fresh header list would first receive the string body's text/plain, making the put_default a no-op. construct_json now materializes init.headers itself and writes the JSON type before the body-derived type is considered.

Bun's existing value text/plain;charset=utf-8 (lowercase) is used, matching what Bun already emits on the response wire for string bodies and on the request wire for Bun.file() bodies.

Verification

New tests in test/js/web/fetch/fetch.test.ts under "Content-Type implied by the body init per fetch spec":

  • fetch() with a string body puts text/plain;charset=utf-8 on the wire (captured by a raw TCP server), both with no headers and with unrelated headers; a non-ASCII string body exercises the InternalBlob { was_string } path; an explicit content-type header wins; URLSearchParams still sends urlencoded; ArrayBuffer / Uint8Array bodies send no Content-Type.
  • fetch(new Request(...)) with a string body sends it on the wire.
  • new Request(...) / new Response(...) with a string, non-ASCII string, or URLSearchParams body expose the type on .headers; an explicit header wins; ArrayBuffer / typed-array bodies do not get a type.

Against unmodified main the four string-body tests fail and the URLSearchParams / ArrayBuffer control tests pass; with the fix all six pass. test/js/bun/util/inspect.test.js is updated for the new Response("Hello") headers now including content-type, matching Node. body.test.ts, bun-serve-static.test.ts, fetch-file-upload.test.ts, FormData.test.ts, and the Response.json tests all pass unchanged.

#32913 touches adjacent lines for a different issue (body-read-before-headers ordering for FormData/URLSearchParams/typed Blob) and explicitly left plain strings alone; this PR addresses the string case.

Fixes #17085
Fixes #8530

@coderabbitai

coderabbitai Bot commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

@robobun, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 10 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 2860c507-9752-4812-9a54-d1a02eecdefa

📥 Commits

Reviewing files that changed from the base of the PR and between 7b46c52 and 911b2a6.

📒 Files selected for processing (7)
  • src/runtime/webcore/Blob.rs
  • src/runtime/webcore/Body.rs
  • src/runtime/webcore/Request.rs
  • src/runtime/webcore/Response.rs
  • test/js/bun/util/inspect.test.js
  • test/js/web/fetch/fetch.test.ts
  • test/vendor.json

Walkthrough

This PR changes how implicit Content-Type is derived from fetch body values, extends Request and Response header handling to use the shared body accessor, and updates tests plus one vendor skip to match the new behavior.

Changes

Implicit Content-Type derivation

Layer / File(s) Summary
Content-type source contract in Blob and Body
src/runtime/webcore/Blob.rs, src/runtime/webcore/Body.rs
Any::has_content_type_from_user() now treats WTFStringImpl and string-backed InternalBlob values as user-provided content types, and Value::content_type() returns implicit Content-Type bytes for Blob, string, and string-backed InternalBlob bodies.
Request header wiring using content_type()
src/runtime/webcore/Request.rs
ensure_fetch_headers, get_content_type, and construct_into now derive Content-Type through body_value().content_type() and seed request headers from that optional value.
Response header wiring using content_type()
src/runtime/webcore/Response.rs
get_or_create_headers, get_content_type, construct_json, and the constructor now use the body accessor to read or write Content-Type, including direct JSON header initialization.
Test and vendor updates
test/js/bun/util/inspect.test.js, test/js/web/fetch/fetch.test.ts, test/vendor.json
The Response inspection snapshot now includes the implied header, fetch tests cover implied and omitted Content-Type cases, and one vendor test is temporarily skipped for the updated Response header behavior.

Possibly related PRs

  • oven-sh/bun#33404: Also propagates derived body Content-Type into response/header state.
🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: string request bodies now send text/plain Content-Type.
Description check ✅ Passed The description covers the change and verification in detail, though it uses custom headings instead of the template's exact sections.
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.

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

@robobun

robobun commented Jul 7, 2026

Copy link
Copy Markdown
Collaborator Author
Updated 7:05 AM PT - Jul 8th, 2026

@robobun, your commit 911b2a6 has 3 failures in Build #70491 (All Failures):


🧪   To try this PR locally:

bunx bun-pr 33677

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

bun-33677 --bun

@github-actions github-actions Bot added the claude label Jul 7, 2026
@github-actions

github-actions Bot commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

Found 2 issues this PR may fix:

  1. Request doesn't set content-type for string body #17085 - Request doesn't set content-type for string body (new Request(url, {method: "POST", body: "Hello World"}).headers.get("content-type") returns null instead of text/plain;charset=UTF-8)
  2. Response with text body should have Content-Type of text/plain #8530 - Response with text body should have Content-Type of text/plain (new Response('yay').headers.get('content-type') returns null instead of text/plain;charset=utf-8)

If this is helpful, copy the block below into the PR description to auto-close these issues on merge.

Fixes #17085
Fixes #8530

🤖 Generated with Claude Code

@coderabbitai coderabbitai 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.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/runtime/webcore/Request.rs (1)

1522-1539: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Confirm the deferred-header case is actually covered end-to-end.

This block only writes Content-Type when req.headers.get().is_some() — i.e., headers were already materialized earlier in construct_into (from an explicit headers init or a cloned Request/Response). When headers are still None here, the implicit content type is deferred to ensure_fetch_headers (lines 250-301), which duplicates this same "only if not already present" logic via a different code path (raw match on body variants vs. content_type()). This is not a bug per se, but it's the second near-duplicate implementation of "derive implicit Content-Type from body and insert if absent" in this file. Worth consolidating into one helper both call, to avoid the two diverging later.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/runtime/webcore/Request.rs` around lines 1522 - 1539, The deferred-header
path and the materialized-header path both implement the same implicit
Content-Type insertion logic in Request::construct_into and
ensure_fetch_headers, so the handling is duplicated and can drift. Extract the
shared “derive Content-Type from body and insert if absent” behavior into one
helper and have both code paths call it, preserving the existing borrowck-safe
handling around req.headers_mut() and body_value().content_type().
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/runtime/webcore/Body.rs`:
- Around line 720-735: String body content-type values are hardcoded with the
wrong charset casing, so update both `Body::content_type()` and
`Internal::content_type()` to return the fetch-spec/browser wire value
`text/plain;charset=UTF-8` for string bodies. Make the change in the string-body
paths (`Value::WTFStringImpl` and the corresponding `Internal` logic) together
so exact header comparisons stay consistent.

---

Outside diff comments:
In `@src/runtime/webcore/Request.rs`:
- Around line 1522-1539: The deferred-header path and the materialized-header
path both implement the same implicit Content-Type insertion logic in
Request::construct_into and ensure_fetch_headers, so the handling is duplicated
and can drift. Extract the shared “derive Content-Type from body and insert if
absent” behavior into one helper and have both code paths call it, preserving
the existing borrowck-safe handling around req.headers_mut() and
body_value().content_type().
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: e6b25484-3ba1-4121-9eea-99d5b7f6bc8c

📥 Commits

Reviewing files that changed from the base of the PR and between 3f5d816 and 9ca7ac3.

📒 Files selected for processing (7)
  • src/runtime/webcore/Blob.rs
  • src/runtime/webcore/Body.rs
  • src/runtime/webcore/Request.rs
  • src/runtime/webcore/Response.rs
  • test/js/bun/util/inspect.test.js
  • test/js/web/fetch/fetch.test.ts
  • test/vendor.json

Comment thread src/runtime/webcore/Body.rs
Comment thread src/runtime/webcore/Body.rs
Comment thread src/runtime/webcore/Body.rs

@coderabbitai coderabbitai 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.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
src/runtime/webcore/Body.rs (1)

725-735: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Derive the string content type from MimeType::TEXT instead of a duplicated literal.

Line 731 hardcodes b"text/plain;charset=utf-8", while the sibling fix in clone_with_readable_stream (lines 1649-1651) sources the same value from bun_http_types::MimeType::TEXT.value.as_ref(). Keeping two literal copies of this spec string invites drift if the constant ever changes.

♻️ Proposed fix
-            Value::WTFStringImpl(_) => Some(b"text/plain;charset=utf-8"),
+            Value::WTFStringImpl(_) => Some(bun_http_types::MimeType::TEXT.value.as_ref()),
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/runtime/webcore/Body.rs` around lines 725 - 735, The Body::content_type
match arm for Value::WTFStringImpl currently hardcodes the text/plain charset
string, creating a duplicated MIME literal. Update that branch to source the
value from bun_http_types::MimeType::TEXT.value.as_ref() just like
clone_with_readable_stream does, so the string content type stays centralized
and consistent.
test/js/web/fetch/fetch.test.ts (1)

2899-2946: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Socket errors are swallowed instead of rejecting the awaited promise.

socket.on("error", () => {}) discards failures silently, so a connection failure leaves nextHead.promise pending until the suite's default timeout instead of failing fast with a clear error. Also note the connection closure reads the outer, reassignable nextHead/lastHead variables rather than capturing them per-connection — safe today only because calls are sequential and no error/close handling exists yet; adding rejection naively on the shared variable risks a stale connection rejecting a later call's promise.

🔧 Proposed fix
 await using server = net.createServer(socket => {
+      const resolver = nextHead;
       let buf = Buffer.alloc(0);
       socket.on("data", d => {
         buf = Buffer.concat([buf, d]);
         if (buf.indexOf("\r\n\r\n") < 0) return;
         lastHead = buf.toString("latin1").split("\r\n\r\n")[0];
         socket.end("HTTP/1.1 200 OK\r\nContent-Length: 0\r\nConnection: close\r\n\r\n");
-        nextHead.resolve();
+        resolver.resolve();
       });
-      socket.on("error", () => {});
+      socket.on("error", err => resolver.reject(err));
     });

As per coding guidelines, "Wire EVERY failure event (error, close, abort, process exit) to reject the awaited promise — never throw inside event callbacks."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/js/web/fetch/fetch.test.ts` around lines 2899 - 2946, The fetch socket
test currently swallows connection failures because socket.on("error", () => {})
ignores errors and leaves doFetch waiting on nextHead.promise until timeout.
Update the server callback in fetch.test.ts so each connection captures its own
resolver state and wires socket error/close (and any other failure path used by
the test) to reject the current Promise.withResolvers instance instead of
relying on shared outer nextHead/lastHead variables. Keep the existing
assertions in doFetch and contentTypeHeader, but make the awaited promise fail
fast with the actual socket failure for the specific request being tested.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@src/runtime/webcore/Body.rs`:
- Around line 725-735: The Body::content_type match arm for Value::WTFStringImpl
currently hardcodes the text/plain charset string, creating a duplicated MIME
literal. Update that branch to source the value from
bun_http_types::MimeType::TEXT.value.as_ref() just like
clone_with_readable_stream does, so the string content type stays centralized
and consistent.

In `@test/js/web/fetch/fetch.test.ts`:
- Around line 2899-2946: The fetch socket test currently swallows connection
failures because socket.on("error", () => {}) ignores errors and leaves doFetch
waiting on nextHead.promise until timeout. Update the server callback in
fetch.test.ts so each connection captures its own resolver state and wires
socket error/close (and any other failure path used by the test) to reject the
current Promise.withResolvers instance instead of relying on shared outer
nextHead/lastHead variables. Keep the existing assertions in doFetch and
contentTypeHeader, but make the awaited promise fail fast with the actual socket
failure for the specific request being tested.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 9df9d7dd-718d-4892-bad3-7d14f2c88887

📥 Commits

Reviewing files that changed from the base of the PR and between 9ca7ac3 and 7b46c52.

📒 Files selected for processing (2)
  • src/runtime/webcore/Body.rs
  • test/js/web/fetch/fetch.test.ts

@robobun

robobun commented Jul 7, 2026

Copy link
Copy Markdown
Collaborator Author

Addressing the two outside-diff review notes:

  • fetch.test.ts socket error handling: applied in 19bbd10. The server callback now captures the resolver per-connection and wires socket.on('error', ...) to reject it, so a mid-request failure fails fast instead of timing out.
  • Body.rs MimeType::TEXT vs literal: left as-is. Value::content_type() returns Option<&[u8]>, and MimeType::TEXT is a const (not static), so MimeType::TEXT.value.as_ref() borrows a temporary Cow that cannot be returned. The inlined literal is the established pattern for this exact case elsewhere in the file set: Blob.rs:6867, Blob.rs:7008-7015, Blob.rs:7096-7102, and Request.rs:718-722 all carry the same comment explaining the const/static distinction. The clone_with_readable_stream site can use MimeType::TEXT because it feeds std::ptr::from_ref (a raw pointer into the static bytes behind the Cow::Borrowed), not a returned borrow.

Comment thread src/runtime/webcore/Response.rs
@robobun

robobun commented Jul 7, 2026

Copy link
Copy Markdown
Collaborator Author

CI status: the diff is green on every lane that exercises it.

Build 70344 (first rebase onto e10d444) completed 285 passed / 1 failed on :darwin: 14 aarch64 with a Puppeteer Chrome DevTools WebSocket launch failure and a JSC DFG JIT segfault at speculationFromValue; neither touches this diff's code paths.

Build 70491 (second rebase onto 332f744): the only hard failure is :windows: 2019 x64-baseline on bake/dev-and-prod.test.ts (render-sentinel timeout) and sql/postgres-binary-array-bounds.test.ts (PostgresError: Failed to connect). Flaky-retry annotations: cli/update_interactive_install.test.ts, cli/hot/hot.test.ts, node/http/node-http-connect.test.ts, sql/postgres-invalid-message-length.test.ts, node/zlib/leak.test.ts, bun/spawn/spawn-pipe-leak.test.ts, napi/napi.test.ts. All Windows/Postgres-connection/memory-threshold flakes unrelated to fetch, Request, Response, or Content-Type.

The pre-rebase builds 69953/69991/70016 each hit :darwin: 26 aarch64 on buildkite-agent artifact download timed out (infra).

The one failure this diff did cause (vendor/elysia/test/core/elysia.test.ts) was addressed via test/vendor.json and has not recurred.

All 11 new tests (fetch.test.ts -t "Content-Type implied by the body init") pass locally and on every CI lane that ran them. Ready for maintainer review.

Comment thread src/runtime/webcore/Body.rs Outdated
Comment thread src/runtime/webcore/Body.rs
@robobun robobun force-pushed the farm/08ef360e/fetch-string-body-content-type branch from 166a2f2 to 5a59f26 Compare July 8, 2026 04:44

@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.

All prior review threads are resolved and I found nothing new, but this is a user-visible Web-compat behavior change across the Request/Response/fetch hot paths (plus a deliberate O(M) clone trade-off for non-ASCII string bodies and a vendor-test skip), so a maintainer should sign off.

Extended reasoning...

Overview

This PR makes string request/response bodies emit Content-Type: text/plain;charset=utf-8 per the WHATWG fetch spec's "extract a body" step, matching Node/undici, Deno, and browsers. It touches four core webcore Rust files (Blob.rs, Body.rs, Request.rs, Response.rs) — adding body::Value::content_type(), updating Any::has_content_type_from_user(), rewiring six header-derivation sites in Request/Response, reworking construct_json to avoid the string body's implied type shadowing application/json, and changing clone_with_readable_stream to preserve was_string through .clone(). Tests, an inspect snapshot update, and a vendor-test skip round it out.

Security risks

None. No auth/crypto/permissions surface. The change adds a header value derived from a compile-time constant; no user-controlled bytes reach the header emission path beyond what already did.

Level of scrutiny

High. This is a behavioral change to production-critical Web API surface (fetch, new Request, new Response, Response.json) that every Bun.serve handler and HTTP client touches. It changes what goes on the wire for the most common body type. The change went through four rounds of my own review with substantive reworks (the .clone() fix, then its over-correction on .blob().type, then the current InternalBlob-preserving approach). The final shape carries a deliberate perf trade-off — non-ASCII string bodies now deep-copy on .clone() instead of sharing a refcounted store — which the author has documented and tied to #33128 as the path back to O(1). That trade-off, the two acknowledged deferred follow-ups (empty-string bodies, consume-before-headers order-dependence via #32913), and the elysia vendor-test skip are all reasonable calls, but they're the kind of calls a maintainer should ratify rather than a bot.

Other factors

Test coverage is thorough (wire-level TCP capture, Request/Response .headers, .clone() on both original and clone, .blob().type non-shadowing guards, ArrayBuffer negative controls). CI is reported green on the lanes that exercise the diff. No outstanding unresolved review threads. The lowercase charset=utf-8 casing choice is intentional and consistent with MimeType::TEXT repo-wide. Given the scope and the number of interacting code paths (lazy vs eager header materialization, clone normalization, .blob() precedence, Response.json), this warrants a human look before merge.

robobun added 4 commits July 8, 2026 12:37
fetch(url, {method: 'POST', body: '<string>'}) was sending the request with
no Content-Type header on the wire, while node/undici, deno, and browsers
all send the spec-mandated text/plain;charset=UTF-8. The same omission made
new Request(...).headers.get('content-type') and new Response('...').headers
return null for string bodies.

The fetch spec's 'extract a body' step assigns text/plain;charset=UTF-8 to
scalar-value-string bodies just as it assigns the urlencoded/multipart types
to URLSearchParams/FormData bodies. Bun's header-building paths gated the
body-derived Content-Type on has_content_type_from_user(), which returned
false for the WTFStringImpl / InternalBlob{was_string} variants a string
body becomes, so nothing was emitted.

Add body::Value::content_type() returning the spec-assigned type for each
body variant and use it at every site that copies the body's type into the
header list (Request construct_into / ensure_fetch_headers / get_content_type,
Response constructor / get_or_create_headers / get_content_type). Update
AnyBlob::has_content_type_from_user() so the fetch wire serializer and
headers_ref helpers see string bodies as having a type. Response.json() now
materializes its headers before the body-derived type is considered so the
JSON type is not overwritten by text/plain.

ArrayBuffer / typed-array bodies (InternalBlob{was_string: false}) keep
returning no type, matching the spec.
elysia 1.4.28's 'automatically handle HEAD request for GET dynamic path'
asserts response.headers.toJSON() is exactly {content-length: '11'} for a
handler returning a plain string. new Response('string').headers now exposes
content-type: text/plain;charset=utf-8 per the fetch spec, matching Node and
browsers, so the assertion sees an extra key.
Value::clone_with_readable_stream converts a non-ASCII string body
(WTFStringImpl -> InternalBlob{was_string:true} -> Blob) without carrying
the was_string bit forward, so the resulting Blob has an empty content_type
and Value::content_type() returns None for both the clone and the mutated
original. Set the Blob's content_type to MimeType::TEXT when was_string,
matching create_with_bytes_and_allocator; dupe_with_content_type copies it
verbatim to the clone.
@robobun robobun force-pushed the farm/08ef360e/fetch-string-body-content-type branch from 5a59f26 to 911b2a6 Compare July 8, 2026 12:42
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.

Request doesn't set content-type for string body Response with text body should have Content-Type of text/plain

1 participant