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>
169 lines
5.4 KiB
JavaScript
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();
|
|
}
|