Something went wrong. Try again.
javascript for 🐜's, a tiny runtime with big ambitions antjs.org
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328// Stream failures that become rejections or callback arguments must not also// escape through uncaughtException. Public callback throws must still escape.const { spawnSync } = require('child_process');const fs = require('fs');const os = require('os');const path = require('path');
function assert(condition, message) { if (!condition) throw new Error(message);}
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'ant-stream-errors-'));let index = 0;
function run(source) { const file = path.join(dir, `case${index++}.cjs`); fs.writeFileSync(file, source); return spawnSync(process.execPath, [file], { encoding: 'utf8', timeout: 5000 });}
const helpers = `function assert(condition, message) { if (!condition) throw new Error(message);}function rejects(promise, expected) { // A JS catch could incidentally consume the stale native exception under test. return promise.then( () => { throw new Error('operation did not reject'); }, error => { if (typeof expected === 'function') assert(error instanceof expected, 'wrong error type'); else assert(error === expected, 'rejection lost its original reason'); } );}`;
const handled = { writableWrite: ` const reason = new Error('sink write'); const writer = new WritableStream({ write() { throw reason; } }).getWriter(); writer.closed.catch(() => {}); await rejects(writer.write('x'), reason); `, writableClose: ` const reason = new Error('sink close'); const writer = new WritableStream({ close() { throw reason; } }).getWriter(); writer.closed.catch(() => {}); await rejects(writer.close(), reason); `, writableAbort: ` const reason = new Error('sink abort'); const writer = new WritableStream({ abort() { throw reason; } }).getWriter(); writer.closed.catch(() => {}); await rejects(writer.abort(), reason); `, writablePrimitive: ` const writer = new WritableStream({ write() { throw undefined; } }).getWriter(); writer.closed.catch(() => {}); await rejects(writer.write('x'), undefined); `, writableSize: ` const reason = new Error('strategy size'); const writer = new WritableStream({}, { size() { throw reason; } }).getWriter(); writer.closed.catch(() => {}); await rejects(writer.write('x'), reason); `, writableInvalidSize: ` const writer = new WritableStream({}, { size() { return -1; } }).getWriter(); writer.closed.catch(() => {}); await rejects(writer.write('x'), RangeError); `, writableSizeConversion: ` const reason = new Error('strategy conversion'); const writer = new WritableStream({}, { size() { return { valueOf() { throw reason; } }; } }).getWriter(); writer.closed.catch(() => {}); await rejects(writer.write('x'), reason); `, writableSizeConversionReasons: ` for (const reason of [undefined, null, 0, 'coercion', new Error('coercion')]) { for (const key of ['valueOf', Symbol.toPrimitive]) { const writer = new WritableStream({}, { size() { return { [key]() { throw reason; } }; } }).getWriter(); writer.closed.catch(() => {}); await rejects(writer.write('x'), reason); } } for (const size of [NaN, Infinity]) { const writer = new WritableStream({}, { size() { return size; } }).getWriter(); writer.closed.catch(() => {}); await rejects(writer.write('x'), RangeError); } `, writableClosed: ` const writer = new WritableStream().getWriter(); await writer.close(); await rejects(writer.write('x'), TypeError); await rejects(writer.close(), TypeError); `, writableLocked: ` const stream = new WritableStream(); const writer = stream.getWriter(); await rejects(stream.abort(), TypeError); await rejects(stream.close(), TypeError); writer.releaseLock(); await rejects(writer.write('x'), TypeError); await rejects(writer.close(), TypeError); await rejects(writer.abort(), TypeError); `, readablePull: ` const reason = new Error('source pull'); const reader = new ReadableStream({ pull() { throw reason; } }).getReader(); reader.closed.catch(() => {}); await rejects(reader.read(), reason); `, readablePullAfterClose: ` const reader = new ReadableStream({ pull(controller) { controller.close(); throw new Error('closed source pull'); } }).getReader(); assert((await reader.read()).done, 'reader did not close'); `, readableCancel: ` const reason = new Error('source cancel'); const stream = new ReadableStream({ cancel() { throw reason; } }); await rejects(stream.cancel(), reason); `, readableLocked: ` const stream = new ReadableStream(); const reader = stream.getReader(); await rejects(stream.cancel(), TypeError); reader.releaseLock(); await rejects(reader.cancel(), TypeError); `, readableSizeConversion: ` const reason = new Error('readable strategy conversion'); let controller; const stream = new ReadableStream({ start(value) { controller = value; } }, { size() { return { valueOf() { throw reason; } }; } }); const reader = stream.getReader(); const closed = rejects(reader.closed, reason); let caught = false; try { controller.enqueue('x'); } catch (error) { caught = true; assert(error === reason, 'enqueue lost its thrown reason'); } assert(caught, 'enqueue did not throw'); await closed; `, readableSizeConversionReasons: ` for (const reason of [undefined, null, 0, 'coercion', new Error('coercion')]) { let controller; const stream = new ReadableStream({ start(c) { controller = c; } }, { size() { return { valueOf() { throw reason; } }; } }); const reader = stream.getReader(); const closed = rejects(reader.closed, reason); let caught = false; try { controller.enqueue('x'); } catch (error) { caught = true; assert(error === reason, 'lost coercion reason'); } assert(caught, 'enqueue did not throw'); await closed; } `, pipeToLocked: ` const source = new ReadableStream(); const reader = source.getReader(); await rejects(source.pipeTo(new WritableStream()), TypeError); reader.releaseLock(); const dest = new WritableStream(); const writer = dest.getWriter(); await rejects(source.pipeTo(dest), TypeError); writer.releaseLock(); `, pipeToInvalid: ` const source = new ReadableStream(); await rejects(source.pipeTo({}), TypeError); await rejects(source.pipeTo(new WritableStream(), { signal: {} }), TypeError); `, pipeToOptionsGetter: ` const reason = new Error('pipe options'); const source = new ReadableStream(); await rejects(source.pipeTo(new WritableStream(), { get signal() { throw reason; } }), reason); `, transformPrimitive: ` const stream = new TransformStream({ transform() { throw 'transform reason'; } }); const reader = stream.readable.getReader(); const writer = stream.writable.getWriter(); reader.closed.catch(() => {}); writer.closed.catch(() => {}); const read = rejects(reader.read(), 'transform reason'); await rejects(writer.write('x'), 'transform reason'); await read; `, zlibCallback: ` await new Promise((resolve) => { require('zlib').gunzip(Buffer.from('invalid gzip'), error => { assert(error instanceof Error, 'zlib callback did not receive an Error'); assert(typeof error.message === 'string', 'zlib callback lost error message'); resolve(); }); }); `, zlibErrorEvent: ` await new Promise((resolve) => { const stream = require('zlib').createGunzip(); stream.on('error', error => { assert(error instanceof Error, 'zlib error event did not receive an Error'); resolve(); }); stream.write(Buffer.from('invalid gzip')); }); `, nodeWriteAfterEnd: ` await new Promise((resolve, reject) => { const stream = new (require('stream').Writable)({ write(chunk, encoding, done) { done(); } }); stream.on('error', reject); stream.end(); setTimeout(() => stream.write('x', error => { assert(error instanceof Error, 'write callback did not receive an Error'); assert(error.message.includes('write after end'), 'write callback lost error message'); resolve(); }), 0); }); `, nodeFinishedInvalid: ` await rejects(require('stream/promises').finished(null), TypeError); `, nodeWriteThrows: ` const { Writable } = require('stream'); for (const reason of [undefined, null, false, 0, 'write failure', new Error('write failure')]) { for (const method of ['write', 'end']) { let callbacks = 0; let events = 0; const stream = new Writable({ write() { throw reason; } }); stream.on('error', () => events++); let caught = false; try { stream[method]('x', () => callbacks++); } catch (error) { caught = true; assert(error === reason, 'lost thrown write reason'); } assert(caught, 'write did not throw'); assert(callbacks === 0, 'throw was delivered as callback completion'); assert(events === 0, 'throw was delivered as an error event'); } } `, nodeBufferedWriteThrows: ` const { Writable } = require('stream'); for (const reason of [undefined, null, new Error('buffered write failure')]) { let completeFirst; let writes = 0; let callbacks = 0; const stream = new Writable({ write(chunk, encoding, done) { if (++writes === 1) completeFirst = done; else throw reason; } }); stream.write('first'); stream.write('second', () => callbacks++); let caught = false; try { completeFirst(); } catch (error) { caught = true; assert(error === reason, 'lost buffered write reason'); } assert(caught, 'buffered write did not throw'); assert(callbacks === 0, 'buffered throw was delivered as callback completion'); } `,};
const escaping = { zlibCallback: ` require('zlib').gunzip(Buffer.from('invalid gzip'), () => { throw new Error('public-callback'); }); `, zlibErrorEvent: ` const stream = require('zlib').createGunzip(); stream.on('error', () => { throw new Error('public-callback'); }); stream.write(Buffer.from('invalid gzip')); `, nodeWriteCallback: ` const stream = new (require('stream').Writable)({ write(chunk, encoding, done) { done(); } }); stream.write('x', () => { throw new Error('public-callback'); }); `, nodeWriteErrorCallback: ` const stream = new (require('stream').Writable)({ write(chunk, encoding, done) { done(); } }); stream.end(); stream.write('x', () => { throw new Error('public-callback'); }); `,};
try { for (const [name, source] of Object.entries(handled)) { for (const listener of [false, true]) { const result = run(helpers + (listener ? `process.on('uncaughtException', e => console.log('UNCAUGHT ' + e));\n` : '') + ` const watchdog = setTimeout(() => { console.error('TIMEOUT'); process.exit(2); }, 1000); (async () => { ${source} clearTimeout(watchdog); console.log('HANDLED'); setTimeout(() => console.log('LATER'), 20); })().catch(error => { console.error('FAIL', error); process.exit(1); }); `); const detail = `${name} listener=${listener}: exit=${result.status}, stdout=${result.stdout}, stderr=${result.stderr}`; assert(result.status === 0, detail); assert(result.stdout.includes('HANDLED') && result.stdout.includes('LATER'), detail); assert(!result.stdout.includes('UNCAUGHT'), detail); assert(result.stderr === '', detail); } }
for (const [name, source] of Object.entries(escaping)) { const result = run(` setTimeout(() => { ${source} }, 0); setTimeout(() => console.log('LATER'), 30); `); const detail = `${name}: exit=${result.status}, stdout=${result.stdout}, stderr=${result.stderr}`; assert(result.status === 1 && result.stderr.includes('public-callback'), detail); assert(!result.stdout.includes('LATER'), detail); }} finally { fs.rmSync(dir, { recursive: true, force: true });}
console.log('PASS');