LibWeb/FileAPI: Handle an aborted stream in Blob::get_stream() close

The streams AO that we were calling to close the stream would assert
if it was not in a readable state. This version of close is exported
publicly in the streams specification, and properly handles this
situation.

Fixes a crash in the imported test, and happens to fix some others!
This commit is contained in:
Shannon Booth 2025-05-31 16:34:13 +12:00 committed by Tim Flynn
commit caf959f06c
Notes: github-actions[bot] 2025-05-31 13:14:02 +00:00
5 changed files with 132 additions and 9 deletions

View file

@ -1,6 +1,6 @@
/*
* Copyright (c) 2022-2024, Kenneth Myhra <kennethmyhra@serenityos.org>
* Copyright (c) 2023, Shannon Booth <shannon@serenityos.org>
* Copyright (c) 2023-2025, Shannon Booth <shannon@serenityos.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
@ -357,7 +357,7 @@ GC::Ref<Streams::ReadableStream> Blob::get_stream()
// FIXME: Spec bug: https://github.com/w3c/FileAPI/issues/206
//
// We need to close the stream so that the stream will finish reading.
Streams::readable_stream_close(*stream);
stream->close();
}));
}
}

View file

@ -0,0 +1,23 @@
Harness status: OK
Found 18 tests
18 Pass
Pass Calling arrayBuffer() on an aborted request
Pass Aborting a request after calling arrayBuffer()
Pass Calling arrayBuffer() on an aborted consumed empty request
Pass Calling arrayBuffer() on an aborted consumed nonempty request
Pass Calling blob() on an aborted request
Pass Aborting a request after calling blob()
Pass Calling blob() on an aborted consumed empty request
Pass Calling blob() on an aborted consumed nonempty request
Pass Calling formData() on an aborted request
Pass Aborting a request after calling formData()
Pass Calling formData() on an aborted consumed nonempty request
Pass Calling json() on an aborted request
Pass Aborting a request after calling json()
Pass Calling json() on an aborted consumed nonempty request
Pass Calling text() on an aborted request
Pass Aborting a request after calling text()
Pass Calling text() on an aborted consumed empty request
Pass Calling text() on an aborted consumed nonempty request

View file

@ -2,8 +2,8 @@ Harness status: OK
Found 40 tests
28 Pass
12 Fail
33 Pass
7 Fail
Pass Consume response's body: from text to text
Pass Consume response's body: from text to blob
Pass Consume response's body: from text to arrayBuffer
@ -13,13 +13,13 @@ Fail Consume response's body: from text with correct multipart type to formData
Pass Consume response's body: from text without correct multipart type to formData (error case)
Pass Consume response's body: from text with correct urlencoded type to formData
Pass Consume response's body: from text without correct urlencoded type to formData (error case)
Fail Consume response's body: from blob to blob
Fail Consume response's body: from blob to text
Fail Consume response's body: from blob to arrayBuffer
Fail Consume response's body: from blob to json
Pass Consume response's body: from blob to blob
Pass Consume response's body: from blob to text
Pass Consume response's body: from blob to arrayBuffer
Pass Consume response's body: from blob to json
Fail Consume response's body: from blob with correct multipart type to formData
Pass Consume response's body: from blob without correct multipart type to formData (error case)
Fail Consume response's body: from blob with correct urlencoded type to formData
Pass Consume response's body: from blob with correct urlencoded type to formData
Pass Consume response's body: from blob without correct urlencoded type to formData (error case)
Pass Consume response's body: from FormData to formData
Pass Consume response's body: from FormData without correct type to formData (error case)

View file

@ -0,0 +1,15 @@
<!doctype html>
<meta charset=utf-8>
<meta name="timeout" content="long">
<script>
self.GLOBAL = {
isWindow: function() { return true; },
isWorker: function() { return false; },
isShadowRealm: function() { return false; },
};
</script>
<script src="../../../resources/testharness.js"></script>
<script src="../../../resources/testharnessreport.js"></script>
<div id=log></div>
<script src="../../../fetch/api/abort/request.any.js"></script>

View file

@ -0,0 +1,85 @@
// META: timeout=long
// META: global=window,worker
const BODY_FUNCTION_AND_DATA = {
arrayBuffer: null,
blob: null,
formData: new FormData(),
json: new Blob(["{}"]),
text: null,
};
for (const [bodyFunction, body] of Object.entries(BODY_FUNCTION_AND_DATA)) {
promise_test(async () => {
const controller = new AbortController();
const signal = controller.signal;
const request = new Request("../resources/data.json", {
method: "post",
signal,
body,
});
controller.abort();
await request[bodyFunction]();
assert_true(
true,
`An aborted request should still be able to run ${bodyFunction}()`
);
}, `Calling ${bodyFunction}() on an aborted request`);
promise_test(async () => {
const controller = new AbortController();
const signal = controller.signal;
const request = new Request("../resources/data.json", {
method: "post",
signal,
body,
});
const p = request[bodyFunction]();
controller.abort();
await p;
assert_true(
true,
`An aborted request should still be able to run ${bodyFunction}()`
);
}, `Aborting a request after calling ${bodyFunction}()`);
if (!body) {
promise_test(async () => {
const controller = new AbortController();
const signal = controller.signal;
const request = new Request("../resources/data.json", {
method: "post",
signal,
body,
});
// consuming happens synchronously, so don't wait
fetch(request).catch(() => {});
controller.abort();
await request[bodyFunction]();
assert_true(
true,
`An aborted consumed request should still be able to run ${bodyFunction}() when empty`
);
}, `Calling ${bodyFunction}() on an aborted consumed empty request`);
}
promise_test(async t => {
const controller = new AbortController();
const signal = controller.signal;
const request = new Request("../resources/data.json", {
method: "post",
signal,
body: body || new Blob(["foo"]),
});
// consuming happens synchronously, so don't wait
fetch(request).catch(() => {});
controller.abort();
await promise_rejects_js(t, TypeError, request[bodyFunction]());
}, `Calling ${bodyFunction}() on an aborted consumed nonempty request`);
}