node/test/es-module/test-wasm-web-api.js
James M Snell 3f81645f8c fs: add autoClose option to FileHandle readableWebStream
By default, the `readableWebStream` method of `FileHandle` returns
a ReadableStream that, when finished, does not close the underlying
FileHandle. This can lead to issues if the stream is consumed
without having a reference to the FileHandle to close after use.
This commit adds an `autoClose` option to the `readableWebStream`
method, which, when set to `true`, will automatically close the
FileHandle when the stream is finished or canceled.

The test modified in this commit demonstrates one of the cases where
this is necessary in that the stream is consumed by separate code than
the FileHandle which was being left to close the underlying fd when
it is garbage collected, which is a deprecated behavior.

PR-URL: https://github.com/nodejs/node/pull/58548
Reviewed-By: LiviaMedeiros <livia@cirno.name>
Reviewed-By: Benjamin Gruenbaum <benjamingr@gmail.com>
2025-06-04 18:26:54 -07:00

249 lines
9.3 KiB
JavaScript

'use strict';
const common = require('../common');
const fixtures = require('../common/fixtures');
const assert = require('assert');
const events = require('events');
const fs = require('fs/promises');
const { createServer } = require('http');
assert.strictEqual(typeof WebAssembly.compileStreaming, 'function');
assert.strictEqual(typeof WebAssembly.instantiateStreaming, 'function');
const simpleWasmBytes = fixtures.readSync('simple.wasm');
// Sets up an HTTP server with the given response handler and calls fetch() to
// obtain a Response from the newly created server.
async function testRequest(handler) {
const server = createServer((_, res) => handler(res)).unref().listen(0);
await events.once(server, 'listening');
const { port } = server.address();
return fetch(`http://127.0.0.1:${port}/foo.wasm`);
}
// Runs the given function both with the promise itself and as a continuation
// of the promise. We use this to test that the API accepts not just a Response
// but also a Promise that resolves to a Response.
function withPromiseAndResolved(makePromise, consume) {
return Promise.all([
consume(makePromise()),
makePromise().then(consume),
]);
}
// The makeResponsePromise function must return a Promise that resolves to a
// Response. The checkResult function receives the Promise returned by
// WebAssembly.compileStreaming and must return a Promise itself.
function testCompileStreaming(makeResponsePromise, checkResult) {
return withPromiseAndResolved(
common.mustCall(makeResponsePromise, 2),
common.mustCall((response) => {
return checkResult(WebAssembly.compileStreaming(response));
}, 2)
);
}
function testCompileStreamingSuccess(makeResponsePromise) {
return testCompileStreaming(makeResponsePromise, async (modPromise) => {
const mod = await modPromise;
assert.strictEqual(mod.constructor, WebAssembly.Module);
});
}
function testCompileStreamingRejection(makeResponsePromise, rejection) {
return testCompileStreaming(makeResponsePromise, (modPromise) => {
assert.strictEqual(modPromise.constructor, Promise);
return assert.rejects(modPromise, rejection);
});
}
function testCompileStreamingSuccessUsingFetch(responseCallback) {
return testCompileStreamingSuccess(() => testRequest(responseCallback));
}
function testCompileStreamingRejectionUsingFetch(responseCallback, rejection) {
return testCompileStreamingRejection(() => testRequest(responseCallback),
rejection);
}
(async () => {
// A non-Response should cause a TypeError.
for (const invalid of [undefined, null, 0, true, 'foo', {}, [], Symbol()]) {
await withPromiseAndResolved(() => Promise.resolve(invalid), (arg) => {
return assert.rejects(() => WebAssembly.compileStreaming(arg), {
name: 'TypeError',
code: 'ERR_INVALID_ARG_TYPE',
message: /^The "source" argument .*$/
});
});
}
// When given a Promise, any rejection should be propagated as-is.
{
const err = new RangeError('foo');
await assert.rejects(() => {
return WebAssembly.compileStreaming(Promise.reject(err));
}, (actualError) => actualError === err);
}
// A valid WebAssembly file with the correct MIME type.
await testCompileStreamingSuccessUsingFetch((res) => {
res.setHeader('Content-Type', 'application/wasm');
res.end(simpleWasmBytes);
});
// The same valid WebAssembly file with the same MIME type, but using a
// Response whose body is a Buffer instead of calling fetch().
await testCompileStreamingSuccess(() => {
return Promise.resolve(new Response(simpleWasmBytes, {
status: 200,
headers: { 'Content-Type': 'application/wasm' }
}));
});
// The same valid WebAssembly file with the same MIME type, but using a
// Response whose body is a ReadableStream instead of calling fetch().
await testCompileStreamingSuccess(async () => {
const handle = await fs.open(fixtures.path('simple.wasm'));
// We set the autoClose option to true so that the file handle is closed
// automatically when the stream is completed or canceled.
const stream = handle.readableWebStream({ autoClose: true });
return Promise.resolve(new Response(stream, {
status: 200,
headers: { 'Content-Type': 'application/wasm' }
}));
});
// A larger valid WebAssembly file with the correct MIME type that causes the
// client to pass it to the compiler in many separate chunks. For this, we use
// the same WebAssembly file as in the previous test but insert useless custom
// sections into the WebAssembly module to increase the file size without
// changing the relevant contents.
await testCompileStreamingSuccessUsingFetch((res) => {
res.setHeader('Content-Type', 'application/wasm');
// Send the WebAssembly magic and version first.
res.write(simpleWasmBytes.slice(0, 8), common.mustCall());
// Construct a 4KiB custom section.
const customSection = Buffer.concat([
Buffer.from([
0, // Custom section.
134, 32, // (134 & 0x7f) + 0x80 * 32 = 6 + 4096 bytes in this section.
5, // The length of the following section name.
]),
Buffer.from('?'.repeat(5)), // The section name
Buffer.from('\0'.repeat(4096)), // The actual section data
]);
// Now repeatedly send useless custom sections. These have no use for the
// WebAssembly compiler but they are syntactically valid. The client has to
// keep reading the stream until the very end to obtain the relevant
// sections within the module. This adds up to a few hundred kibibytes.
(function next(i) {
if (i < 100) {
while (res.write(customSection));
res.once('drain', () => next(i + 1));
} else {
// End the response body with the actual module contents.
res.end(simpleWasmBytes.slice(8));
}
})(0);
});
// A valid WebAssembly file with an empty parameter in the (otherwise valid)
// MIME type.
await testCompileStreamingRejectionUsingFetch((res) => {
res.setHeader('Content-Type', 'application/wasm;');
res.end(simpleWasmBytes);
}, {
name: 'TypeError',
code: 'ERR_WEBASSEMBLY_RESPONSE',
message: 'WebAssembly response has unsupported MIME type ' +
"'application/wasm;'"
});
// A valid WebAssembly file with an invalid MIME type.
await testCompileStreamingRejectionUsingFetch((res) => {
res.setHeader('Content-Type', 'application/octet-stream');
res.end(simpleWasmBytes);
}, {
name: 'TypeError',
code: 'ERR_WEBASSEMBLY_RESPONSE',
message: 'WebAssembly response has unsupported MIME type ' +
"'application/octet-stream'"
});
// HTTP status code indicating an error.
await testCompileStreamingRejectionUsingFetch((res) => {
res.statusCode = 418;
res.setHeader('Content-Type', 'application/wasm');
res.end(simpleWasmBytes);
}, {
name: 'TypeError',
code: 'ERR_WEBASSEMBLY_RESPONSE',
message: /^WebAssembly response has status code 418$/
});
// HTTP status code indicating an error, but using a Response whose body is
// a Buffer instead of calling fetch().
await testCompileStreamingSuccess(() => {
return Promise.resolve(new Response(simpleWasmBytes, {
status: 200,
headers: { 'Content-Type': 'application/wasm' }
}));
});
// Extra bytes after the WebAssembly file.
await testCompileStreamingRejectionUsingFetch((res) => {
res.setHeader('Content-Type', 'application/wasm');
res.end(Buffer.concat([simpleWasmBytes, Buffer.from('foo')]));
}, {
name: 'CompileError',
message: /^WebAssembly\.compileStreaming\(\): .*$/
});
// Missing bytes at the end of the WebAssembly file.
await testCompileStreamingRejectionUsingFetch((res) => {
res.setHeader('Content-Type', 'application/wasm');
res.end(simpleWasmBytes.subarray(0, simpleWasmBytes.length - 3));
}, {
name: 'CompileError',
message: /^WebAssembly\.compileStreaming\(\): .*$/
});
// Incomplete HTTP response body. The TypeError might come as a surprise, but
// it originates from within fetch().
await testCompileStreamingRejectionUsingFetch((res) => {
res.setHeader('Content-Length', simpleWasmBytes.length);
res.setHeader('Content-Type', 'application/wasm');
res.write(simpleWasmBytes.slice(0, 5), common.mustSucceed(() => {
res.destroy();
}));
}, {
name: 'TypeError',
message: /terminated/
});
// Test "Developer-Facing Display Conventions" described in the WebAssembly
// Web API specification.
await testCompileStreaming(() => testRequest((res) => {
// Respond with a WebAssembly module that only exports a single function,
// which only contains an 'unreachable' instruction.
res.setHeader('Content-Type', 'application/wasm');
res.end(fixtures.readSync('crash.wasm'));
}), async (modPromise) => {
// Call the WebAssembly function and check that the error stack contains the
// correct "WebAssembly location" as per the specification.
const mod = await modPromise;
const instance = new WebAssembly.Instance(mod);
assert.throws(() => instance.exports.crash(), (err) => {
const stack = err.stack.split(/\n/g);
assert.strictEqual(stack[0], 'RuntimeError: unreachable');
assert.match(stack[1],
/^\s*at http:\/\/127\.0\.0\.1:\d+\/foo\.wasm:wasm-function\[0\]:0x22$/);
return true;
});
});
})().then(common.mustCall());