Skip to content

feat: add Blob/Body textStream() - #35616

Merged
bartlomieju merged 4 commits into
denoland:mainfrom
nathanwhitbot:orcha/impl-7bf43cdd
Aug 3, 2026
Merged

bartlomieju merged 4 commits into
denoland:mainfrom
nathanwhitbot:orcha/impl-7bf43cdd

Conversation

@nathanwhitbot

@nathanwhitbot nathanwhitbot commented Jun 29, 2026 •

Copy link
Copy Markdown
Contributor

Implements the textStream() convenience method on Blob/File and on the fetch Body mixin (Request/Response), per the standard algorithms:

textStream() returns a ReadableStream<string> obtained by piping the binary body/blob stream through a fresh TextDecoderStream. It always decodes as UTF-8 (ignoring any Content-Type/charset), making it a convenient shorthand for obj.stream().pipeThrough(new TextDecoderStream()).

Changes

  • ext/fetch/22_body.js — add textStream() to the mixinBody mixin. Throws TypeError if the body is unusable (already consumed/locked) and returns an empty, already-closed stream when the body is null.
  • ext/web/09_file.js — add Blob.prototype.textStream() (inherited by File). TextDecoderStream is loaded lazily, mirroring the existing lazy ReadableStream wrapper, to preserve the deferred-load of the streams polyfill at startup.
  • cli/tsc/dts/lib.deno_fetch.d.ts / cli/tsc/dts/lib.deno_web.d.ts — type declarations.
  • tests/unit/blob_test.ts, tests/unit/body_test.ts — unit tests covering UTF-8 (multi-byte) decoding, consumed-body TypeError, and the null-body case.

Closes #34792

Implements the `textStream()` convenience method that returns a
`ReadableStream<string>` produced by piping the binary body/blob stream
through a new `TextDecoderStream` (always UTF-8, ignoring any
Content-Type/charset), per whatwg/fetch#1862 and w3c/FileAPI#221.

- `Blob.prototype.textStream()` (inherited by `File`) in ext/web/09_file.js,
  using a lazy `TextDecoderStream` wrapper that mirrors the existing lazy
  `ReadableStream` wrapper so the streams polyfill is not loaded eagerly at
  module-body time.
- `textStream()` on the fetch `Body` mixin (Request/Response) in
  ext/fetch/22_body.js. Throws `TypeError` when the body is already consumed;
  a null body yields an empty, already-closed stream that reads to "".
- Type declarations in lib.deno_web.d.ts and lib.deno_fetch.d.ts.
- Unit tests in tests/unit/blob_test.ts and tests/unit/body_test.ts covering
  UTF-8 (multi-byte) decoding, Response/Request bodies, the consumed-body
  TypeError, and the null-body empty-stream case.

Closes denoland#34792
@petamoriken

Copy link
Copy Markdown
Contributor

It looks like we need to update the wpt expectations (#35803)

bartlomieju and others added 2 commits August 3, 2026 01:17
The Blob-textStream WPT tests and the textStream() idlharness subtests were
listed as expected failures. Now that textStream() is implemented they pass,
which the WPT runner reports as a failure ("expected test failures that
passed"). Flip the two Blob-textStream files to passing and drop the
textStream() entries from the idlharness expectedFailures lists.
@nathanwhitbot

Copy link
Copy Markdown
Contributor Author

Pushed 89fb367 to fix the failing wpt release linux-x86_64 check.

tests/wpt/runner/expectations/FileAPI.json still listed Blob-textStream.any.{html,worker.html} and the Blob interface: operation textStream() idlharness subtests as expected failures. With textStream() implemented they now pass, and the runner reports "expected test failures that passed" as a failure. The commit flips those two files to true and drops the four textStream() entries from the idlharness* expectedFailures lists — matching the corrected-expectations diff the runner emitted in the CI log.

@bartlomieju bartlomieju left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Checked the implementation against the merged spec algorithms — the unusable-first check, the null-body branch and the always-UTF-8 decoder all line up, and inner.stream materializing static bodies means pipeThrough is safe on that path. One small deviation inline.

Unrelated to the code: both proposals have merged (whatwg/fetch#1862 and w3c/FileAPI#221), so the "in-progress standards proposals" framing in the PR description is out of date — worth fixing since that text becomes the squashed commit message.

Comment thread ext/fetch/22_body.js Outdated
Comment on lines +416 to +425
let stream;
if (inner !== null) {
stream = inner.stream;
} else {
// A null body yields an empty, already-closed stream so that reading
// it to completion produces "".
stream = new ReadableStream();
readableStreamClose(stream);
}
return stream.pipeThrough(new TextDecoderStream());

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Small deviation from the spec here. The null-body branch in the merged algorithm creates an empty stream, closes it, and returns it — it doesn't fall through to the decoder:

  1. If this's body is null:
    1. Let emptyStream be a new ReadableStream in this's relevant realm.
    2. Set up emptyStream.
    3. Close emptyStream.
    4. Return emptyStream.

The piping is only steps 3-6, on the non-null path. It isn't observably different — a closed stream piped through TextDecoderStream also yields zero chunks, since the transform's flush guards with if (final) — but returning it directly matches the spec exactly and avoids constructing a TextDecoderStream (and therefore a TransformStream, which is what actually pulls in the streams polyfill) for a body that has nothing to decode.

Suggested change
let stream;
if (inner !== null) {
stream = inner.stream;
} else {
// A null body yields an empty, already-closed stream so that reading
// it to completion produces "".
stream = new ReadableStream();
readableStreamClose(stream);
}
return stream.pipeThrough(new TextDecoderStream());
if (inner === null) {
// A null body yields an empty, already-closed stream. Per the spec
// this is returned as-is — no decoder is set up for it.
const emptyStream = new ReadableStream();
readableStreamClose(emptyStream);
return emptyStream;
}
return inner.stream.pipeThrough(new TextDecoderStream());

Per the merged textStream() algorithm, the null-body branch returns the
empty closed stream as-is rather than piping it through a decoder. Not
observably different, but it matches the spec and avoids constructing a
TextDecoderStream (and hence a TransformStream, which pulls in the
streams polyfill) for a body with nothing to decode.
@nathanwhitbot

Copy link
Copy Markdown
Contributor Author

Applied the suggestion in 9d68dbf — the null-body branch now returns the closed empty stream directly instead of falling through to the decoder, matching steps 2.1-2.4 of the merged algorithm and skipping the TextDecoderStream/TransformStream construction on that path.

One adjustment to the suggested snippet: the — in the comment had to become an ASCII ;. Extension sources are asserted to be 7-bit ASCII at snapshot time, so the em dash failed the build with Extension code must be 7-bit ASCII: ext:deno_fetch/22_body.js (found —).

Also rewrote the PR description to reflect that both proposals have merged — it now links the merged algorithms directly rather than describing them as in-progress.

cargo test unit::body_test and unit::blob_test both pass.

@bartlomieju bartlomieju changed the title feat(ext/web,ext/fetch): add Blob/Body textStream() feat: add Blob/Body textStream() Aug 3, 2026
@bartlomieju
bartlomieju merged commit 8da7d1f into denoland:main Aug 3, 2026
138 checks passed
bartlomieju added a commit that referenced this pull request Aug 6, 2026
Closes #34792

---------

Co-authored-by: Nathan Whitaker <nathan@deno.com>
Co-authored-by: Bartek Iwańczuk <biwanczuk@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Blob/Body textStream()

4 participants