node/test/parallel/test-quic-session-close-error-code.mjs
Tim Perry e1ae3a56f9
quic: add proper error codes & messages for QUIC failures
Signed-off-by: Tim Perry <pimterry@gmail.com>
PR-URL: https://github.com/nodejs/node/pull/63198
Reviewed-By: Ethan Arrowood <ethan@arrowood.dev>
Reviewed-By: Stephen Belanger <admin@stephenbelanger.com>
2026-05-28 20:02:05 +00:00

169 lines
5.4 KiB
JavaScript

// 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';
const { strictEqual, rejects } = assert;
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) => {
strictEqual(err.code, 'ERR_QUIC_APPLICATION_ERROR');
strictEqual(err.message.includes('42'), true,
'error message should contain the code');
strictEqual(err.message.includes('client shutdown'), true,
'error message should contain the reason');
strictEqual(err.errorCode, 42n);
strictEqual(err.type, 'application');
strictEqual(err.reason, 'client shutdown');
});
await 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) => {
strictEqual(err.code, 'ERR_QUIC_TRANSPORT_ERROR');
strictEqual(err.message.includes('1'), true,
'error message should contain the code');
strictEqual(err.errorCode, 1n);
strictEqual(err.type, 'transport');
});
await 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) => {
strictEqual(err.code, 'ERR_QUIC_APPLICATION_ERROR');
strictEqual(err.message.includes('99'), true);
strictEqual(err.errorCode, 99n);
strictEqual(err.type, 'application');
strictEqual(err.reason, 'destroy with code');
});
await 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.
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 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();
}