From 26bec7cc620efbe7f0a2bf087a62b2650dcaaf3d Mon Sep 17 00:00:00 2001 From: crowlbot <280062030+crowlbot@users.noreply.github.com> Date: Fri, 5 Jun 2026 01:50:00 +0000 Subject: [PATCH 1/3] fix(ext/webidl): follow GetIterator semantics in async iterable converter The async iterable converter used by ReadableStream.from() rejected non-Object values and only treated an `undefined` (not `null`) @@asyncIterator / @@iterator as an absent method. Per the GetIterator(obj, async) abstract operation, the iterator methods are looked up with GetMethod semantics, which (1) performs ToObject on the value, so primitives such as strings are accepted, and (2) treats both `undefined` and `null` as an absent method. Fixes two WPT subtests in streams/readable-streams/from.any: - ReadableStream.from accepts a string - ReadableStream.from ignores a null @@asyncIterator --- ext/webidl/00_webidl.js | 10 +++++++--- tests/wpt/runner/expectations/streams.json | 14 ++------------ 2 files changed, 9 insertions(+), 15 deletions(-) diff --git a/ext/webidl/00_webidl.js b/ext/webidl/00_webidl.js index 6ce102f669cfb1..995fc648bb18a2 100644 --- a/ext/webidl/00_webidl.js +++ b/ext/webidl/00_webidl.js @@ -998,7 +998,7 @@ function createAsyncIterableConverter(converter) { context = undefined, opts = EMPTY_OPTS, ) { - if (type(V) !== "Object") { + if (V === undefined || V === null) { throw makeException( TypeError, "can not be converted to async iterable.", @@ -1007,12 +1007,16 @@ function createAsyncIterableConverter(converter) { ); } + // Follows the GetIterator(obj, async) abstract operation: the @@asyncIterator + // and @@iterator methods are looked up with GetMethod semantics, which treats + // both `undefined` and `null` as an absent method, and which performs ToObject + // on `V` (so primitives such as strings are accepted). let isAsync = true; let method = V[SymbolAsyncIterator]; - if (method === undefined) { + if (method === undefined || method === null) { method = V[SymbolIterator]; - if (method === undefined) { + if (method === undefined || method === null) { throw makeException( TypeError, "is not iterable.", diff --git a/tests/wpt/runner/expectations/streams.json b/tests/wpt/runner/expectations/streams.json index 82a04140477983..9a9762aac8671a 100644 --- a/tests/wpt/runner/expectations/streams.json +++ b/tests/wpt/runner/expectations/streams.json @@ -115,18 +115,8 @@ "templated.any.worker.html": true, "async-iterator.any.worker.html": true, "cross-realm-crash.window.html": false, - "from.any.html": { - "expectedFailures": [ - "ReadableStream.from ignores a null @@asyncIterator", - "ReadableStream.from accepts a string" - ] - }, - "from.any.worker.html": { - "expectedFailures": [ - "ReadableStream.from ignores a null @@asyncIterator", - "ReadableStream.from accepts a string" - ] - }, + "from.any.html": true, + "from.any.worker.html": true, "crashtests": { "garbage-collection.any.html": true, "garbage-collection.any.worker.html": true From 486ed6acae72cb0b81ec5283297f5d5860d75da0 Mon Sep 17 00:00:00 2001 From: crowlbot <280062030+crowlbot@users.noreply.github.com> Date: Fri, 5 Jun 2026 06:33:00 +0000 Subject: [PATCH 2/3] test: update ReadableStream.from string test to spec behavior The unit test asserted ReadableStream.from(string) throws, which was the pre-spec behavior. Per the Streams spec it accepts any iterable, including strings; update the test to assert characters are yielded. --- tests/unit/streams_test.ts | 22 +++++++++------------- 1 file changed, 9 insertions(+), 13 deletions(-) diff --git a/tests/unit/streams_test.ts b/tests/unit/streams_test.ts index d08a79dd3fb504..96817306b6ccbd 100644 --- a/tests/unit/streams_test.ts +++ b/tests/unit/streams_test.ts @@ -1,10 +1,5 @@ // Copyright 2018-2026 the Deno authors. MIT license. -import { - assertEquals, - assertRejects, - assertThrows, - fail, -} from "./test_util.ts"; +import { assertEquals, assertRejects, fail } from "./test_util.ts"; // `resourceForReadableStream` is registered on the internals object only when // `ext:deno_web/06_streams.js` first evaluates, which is now lazy. Touch the @@ -578,13 +573,14 @@ Deno.test(async function decompressionStreamInvalidGzipStillReported() { ); }); -Deno.test(function readableStreamFromWithStringThrows() { - assertThrows( - // @ts-expect-error: primitives are not acceptable - () => ReadableStream.from("string"), - TypeError, - "Failed to execute 'ReadableStream.from': Argument 1 can not be converted to async iterable.", - ); +Deno.test(async function readableStreamFromString() { + // Per the Streams spec, ReadableStream.from() accepts any iterable, including + // strings, which are iterated by code point. + const stream = ReadableStream.from("ab"); + const reader = stream.getReader(); + assertEquals(await reader.read(), { value: "a", done: false }); + assertEquals(await reader.read(), { value: "b", done: false }); + assertEquals((await reader.read()).done, true); }); Deno.test(async function readableStreamFromWithStringThrows() { From ab39e8627b9a991123db19ea6573c3efe5952654 Mon Sep 17 00:00:00 2001 From: crowlbot <280062030+crowlbot@users.noreply.github.com> Date: Fri, 5 Jun 2026 09:34:55 +0000 Subject: [PATCH 3/3] test: suppress static type error for string in ReadableStream.from test The ReadableStream.from type signature requires an object, so passing a string primitive is a static type error even though it is iterable at runtime. Add a @ts-expect-error directive (matching the prior test). --- tests/unit/streams_test.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/unit/streams_test.ts b/tests/unit/streams_test.ts index 96817306b6ccbd..c2a5d155dc1f67 100644 --- a/tests/unit/streams_test.ts +++ b/tests/unit/streams_test.ts @@ -575,7 +575,9 @@ Deno.test(async function decompressionStreamInvalidGzipStillReported() { Deno.test(async function readableStreamFromString() { // Per the Streams spec, ReadableStream.from() accepts any iterable, including - // strings, which are iterated by code point. + // strings, which are iterated by code point. The TS type requires an object, + // so a string primitive is rejected statically even though it works at runtime. + // @ts-expect-error: string is iterable at runtime but the type requires an object const stream = ReadableStream.from("ab"); const reader = stream.getReader(); assertEquals(await reader.read(), { value: "a", done: false });