// Flags: --experimental-quic --no-warnings // Test: session close/destroy with application and transport error codes // . // Application error propagated as ERR_QUIC_APPLICATION_ERROR. // Session close with specific app error code — peer receives it. // Verifies that close() and destroy() with { code, type, reason } options // send the correct CONNECTION_CLOSE frame, and the peer receives the // correct error type, code, and reason. import { hasQuic, skip, mustCall } from '../common/index.mjs'; import assert from 'node:assert'; import { setTimeout } from 'node:timers/promises'; if (!hasQuic) { skip('QUIC is not enabled'); } const { listen, connect } = await import('../common/quic.mjs'); // --- Test 1: close() with application error code --- // The client closes with an application error. The server receives // ERR_QUIC_APPLICATION_ERROR with the exact code and reason. { const serverGot = Promise.withResolvers(); const serverEndpoint = await listen(mustCall(async (serverSession) => { serverSession.onerror = mustCall((err) => { assert.strictEqual(err.code, 'ERR_QUIC_APPLICATION_ERROR'); assert.strictEqual(err.message.includes('42'), true); // Error message should contain the code assert.strictEqual(err.message.includes('client shutdown'), true); // Error message should contain the reason assert.strictEqual(err.errorCode, 42n); assert.strictEqual(err.type, 'application'); assert.strictEqual(err.reason, 'client shutdown'); }); await assert.rejects(serverSession.closed, { code: 'ERR_QUIC_APPLICATION_ERROR', errorCode: 42n, reason: 'client shutdown', }); serverGot.resolve(); })); const clientSession = await connect(serverEndpoint.address, { reuseEndpoint: false, }); await clientSession.opened; // Small delay to ensure handshake is fully confirmed so ngtcp2 // generates the 1-RTT APPLICATION CONNECTION_CLOSE frame. await setTimeout(100); // close() with application error — the client's closed promise // resolves because the close was locally initiated (intentional). // The peer receives the error code, but the local side is not in error. await clientSession.close({ code: 42n, type: 'application', reason: 'client shutdown', }); await serverGot.promise; await serverEndpoint.close(); } // --- Test 2: close() with transport error code --- // The client closes with a transport error. The server receives // ERR_QUIC_TRANSPORT_ERROR with the exact code. { const serverGot = Promise.withResolvers(); const serverEndpoint = await listen(mustCall(async (serverSession) => { serverSession.onerror = mustCall((err) => { assert.strictEqual(err.code, 'ERR_QUIC_TRANSPORT_ERROR'); assert.strictEqual(err.message.includes('1'), true); // Error message should contain the code assert.strictEqual(err.errorCode, 1n); assert.strictEqual(err.type, 'transport'); }); await assert.rejects(serverSession.closed, { code: 'ERR_QUIC_TRANSPORT_ERROR', }); serverGot.resolve(); })); const clientSession = await connect(serverEndpoint.address, { reuseEndpoint: false, }); await clientSession.opened; await setTimeout(100); // close() with transport error — resolves locally (intentional). await clientSession.close({ code: 1n }); await serverGot.promise; await serverEndpoint.close(); } // --- Test 3: destroy() with application error code --- // The client destroys with both a JS error and a QUIC error code. // The peer receives the QUIC application error. { const serverGot = Promise.withResolvers(); const serverEndpoint = await listen(mustCall(async (serverSession) => { serverSession.onerror = mustCall((err) => { assert.strictEqual(err.code, 'ERR_QUIC_APPLICATION_ERROR'); assert.strictEqual(err.message.includes('99'), true); assert.strictEqual(err.errorCode, 99n); assert.strictEqual(err.type, 'application'); assert.strictEqual(err.reason, 'destroy with code'); }); await assert.rejects(serverSession.closed, { code: 'ERR_QUIC_APPLICATION_ERROR', }); serverGot.resolve(); })); const clientSession = await connect(serverEndpoint.address, { reuseEndpoint: false, onerror: mustCall((err) => { // The JS error passed to destroy is delivered via onerror. assert.strictEqual(err.message, 'fatal error'); }), }); await clientSession.opened; await setTimeout(100); const jsError = new Error('fatal error'); clientSession.destroy(jsError, { code: 99n, type: 'application', reason: 'destroy with code', }); // The closed promise rejects with the JS error, not the QUIC error. await assert.rejects(clientSession.closed, jsError); await serverGot.promise; await serverEndpoint.close(); } // --- Test 4: close() with no options (default behavior) --- // Verify the default close sends NO_ERROR and the peer closes cleanly. { const serverGot = Promise.withResolvers(); const serverEndpoint = await listen(mustCall(async (serverSession) => { // No onerror — clean close should not trigger errors. await serverSession.closed; serverGot.resolve(); })); const clientSession = await connect(serverEndpoint.address, { reuseEndpoint: false, }); await clientSession.opened; await setTimeout(100); // Default close — no error code, clean shutdown. await clientSession.close(); await serverGot.promise; await serverEndpoint.close(); }