Skip to content

Commit 8cc1438

Browse files
authored
lib: fix blob.stream() causing hanging promises
Refs: #47993 (comment) PR-URL: #48232 Reviewed-By: James M Snell <jasnell@gmail.com> Reviewed-By: Matteo Collina <matteo.collina@gmail.com> Reviewed-By: Benjamin Gruenbaum <benjamingr@gmail.com>
1 parent 2c6698b commit 8cc1438

File tree

2 files changed

+85
-29
lines changed

2 files changed

+85
-29
lines changed

lib/internal/blob.js

+39-28
Original file line numberDiff line numberDiff line change
@@ -329,34 +329,45 @@ class Blob {
329329
pull(c) {
330330
const { promise, resolve, reject } = createDeferredPromise();
331331
this.pendingPulls.push({ resolve, reject });
332-
reader.pull((status, buffer) => {
333-
// If pendingPulls is empty here, the stream had to have
334-
// been canceled, and we don't really care about the result.
335-
// we can simply exit.
336-
if (this.pendingPulls.length === 0) {
337-
return;
338-
}
339-
const pending = this.pendingPulls.shift();
340-
if (status === 0) {
341-
// EOS
342-
c.close();
343-
pending.resolve();
344-
return;
345-
} else if (status < 0) {
346-
// The read could fail for many different reasons when reading
347-
// from a non-memory resident blob part (e.g. file-backed blob).
348-
// The error details the system error code.
349-
const error = lazyDOMException('The blob could not be read', 'NotReadableError');
350-
351-
c.error(error);
352-
pending.reject(error);
353-
return;
354-
}
355-
if (buffer !== undefined) {
356-
c.enqueue(new Uint8Array(buffer));
357-
}
358-
pending.resolve();
359-
});
332+
const readNext = () => {
333+
reader.pull((status, buffer) => {
334+
// If pendingPulls is empty here, the stream had to have
335+
// been canceled, and we don't really care about the result.
336+
// We can simply exit.
337+
if (this.pendingPulls.length === 0) {
338+
return;
339+
}
340+
if (status === 0) {
341+
// EOS
342+
c.close();
343+
const pending = this.pendingPulls.shift();
344+
pending.resolve();
345+
return;
346+
} else if (status < 0) {
347+
// The read could fail for many different reasons when reading
348+
// from a non-memory resident blob part (e.g. file-backed blob).
349+
// The error details the system error code.
350+
const error = lazyDOMException('The blob could not be read', 'NotReadableError');
351+
const pending = this.pendingPulls.shift();
352+
c.error(error);
353+
pending.reject(error);
354+
return;
355+
}
356+
if (buffer !== undefined) {
357+
c.enqueue(new Uint8Array(buffer));
358+
}
359+
// We keep reading until we either reach EOS, some error, or we
360+
// hit the flow rate of the stream (c.desiredSize).
361+
queueMicrotask(() => {
362+
if (c.desiredSize <= 0) {
363+
// A manual backpressure check.
364+
return;
365+
}
366+
readNext();
367+
});
368+
});
369+
};
370+
readNext();
360371
return promise;
361372
},
362373
cancel(reason) {

test/parallel/test-blob.js

+46-1
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
1-
// Flags: --no-warnings
1+
// Flags: --no-warnings --expose-internals
22
'use strict';
33

44
const common = require('../common');
55
const assert = require('assert');
66
const { Blob } = require('buffer');
77
const { inspect } = require('util');
88
const { EOL } = require('os');
9+
const { kState } = require('internal/webstreams/util');
910

1011
{
1112
const b = new Blob();
@@ -237,6 +238,50 @@ assert.throws(() => new Blob({}), {
237238
assert(res.done);
238239
})().then(common.mustCall());
239240

241+
(async () => {
242+
const b = new Blob(Array(10).fill('hello'));
243+
const reader = b.stream().getReader();
244+
const chunks = [];
245+
while (true) {
246+
const res = await reader.read();
247+
if (res.done) break;
248+
assert.strictEqual(res.value.byteLength, 5);
249+
chunks.push(res.value);
250+
}
251+
assert.strictEqual(chunks.length, 10);
252+
})().then(common.mustCall());
253+
254+
(async () => {
255+
const b = new Blob(Array(10).fill('hello'));
256+
const reader = b.stream().getReader();
257+
const chunks = [];
258+
while (true) {
259+
const res = await reader.read();
260+
if (chunks.length === 5) {
261+
reader.cancel('boom');
262+
break;
263+
}
264+
if (res.done) break;
265+
assert.strictEqual(res.value.byteLength, 5);
266+
chunks.push(res.value);
267+
}
268+
assert.strictEqual(chunks.length, 5);
269+
reader.closed.then(common.mustCall());
270+
})().then(common.mustCall());
271+
272+
(async () => {
273+
const b = new Blob(Array(10).fill('hello'));
274+
const stream = b.stream();
275+
const reader = stream.getReader();
276+
assert.strictEqual(stream[kState].controller.desiredSize, 1);
277+
const { value, done } = await reader.read();
278+
assert.strictEqual(value.byteLength, 5);
279+
assert(!done);
280+
setTimeout(() => {
281+
assert.strictEqual(stream[kState].controller.desiredSize, 0);
282+
}, 0);
283+
})().then(common.mustCall());
284+
240285
{
241286
const b = new Blob(['hello\n'], { endings: 'native' });
242287
assert.strictEqual(b.size, EOL.length + 5);

0 commit comments

Comments
 (0)