@xof/quic
v1.0.0
Published
Low-level QUIC v1/v2 and HTTP/3 protocol stack for Node.js with TLS 1.3, recovery, streams, QPACK, diagnostics, QLog, and interoperability tooling.
Readme
@xof/quic
Low-level QUIC v1/v2 and HTTP/3 protocol stack for Node.js.
@xof/quic exposes the transport layer directly, plus an HTTP/3 layer built on top of the same QUIC connection. The package is CommonJS-first and has no runtime dependencies.
Requirements
- Node.js
>= 20 - UDP access to the target host/port
Installation
npm install @xof/quicImport
The package uses CommonJS:
const {
QUICClient,
QUICServer,
QUICConnection,
HTTP3Connection,
attachHTTP3
} = require('@xof/quic');QUIC server (PSK mode)
QUICServer listens on UDP. By default it uses QUIC v1, ALPN xof, and the built-in PSK value unless psk is provided.
'use strict';
const { QUICServer } = require('@xof/quic');
const server = new QUICServer({
host: '127.0.0.1',
port: 4433,
psk: process.env.XOF_QUIC_PSK || 'xof-quic-local-dev-psk-change-me',
identity: process.env.XOF_QUIC_IDENTITY || 'xof',
alpn: 'xof'
});
server.on('listening', ({ address, port }) => {
console.log(`QUIC server listening on ${address}:${port}`);
});
server.on('connection', conn => {
console.log('QUIC connection established');
conn.on('data', (data, connection, streamId) => {
console.log(`stream ${streamId}:`, data.toString());
connection.write(data, {
streamId,
fin: true
});
});
conn.on('error', err => {
console.error('[connection error]', err);
});
conn.on('close', info => {
console.log('QUIC connection closed:', info);
});
});
server.on('clientError', err => {
console.error('[clientError]', err);
});
server.on('error', err => {
console.error('[server error]', err);
});
server.listen().catch(err => {
console.error(err);
process.exitCode = 1;
});QUIC client
connect() resolves to a QUICConnection.
'use strict';
const { QUICClient } = require('@xof/quic');
(async () => {
const client = new QUICClient({
host: '127.0.0.1',
port: 4433,
psk: process.env.XOF_QUIC_PSK || 'xof-quic-local-dev-psk-change-me',
identity: process.env.XOF_QUIC_IDENTITY || 'xof',
alpn: 'xof',
handshakeTimeout: 15000
});
try {
const conn = await client.connect();
conn.on('ready', () => {
console.log('QUIC ready');
});
conn.on('data', (data, connection, streamId) => {
console.log(`stream ${streamId}:`, data.toString());
});
conn.on('error', err => {
console.error('[connection error]', err);
});
const stream = conn.openStream();
stream.end(Buffer.from('hello from xof-quic'));
} catch (err) {
console.error('[client error]', err);
}
setTimeout(() => {
client.close();
}, 500);
})();Stream API
After a connection is open, QUICConnection exposes:
const stream = conn.openStream();
stream.id;
stream.write(Buffer.from('part 1'));
stream.end(Buffer.from('part 2'));
stream.reset(0);For a unidirectional stream:
const stream = conn.openUnidirectionalStream();
stream.write(Buffer.from('hello'));
stream.end();
stream.reset(0);The connection also provides direct transport helpers:
conn.write(Buffer.from('hello'), {
streamId: 0n,
fin: true
});
conn.sendDatagram(Buffer.from('ping'));
conn.resetStream(0n, 0);
conn.updateKeys();
conn.close();sendDatagram() requires the peer to have negotiated a non-zero maxDatagramFrameSize.
HTTP/3
HTTP/3 is attached to an already-established QUICConnection with attachHTTP3().
The repository's HTTP/3 server follows this exact pattern:
'use strict';
const { QUICServer, attachHTTP3 } = require('@xof/quic');
const server = new QUICServer({
host: '0.0.0.0',
port: 4433,
alpn: 'h3',
noPsk: true,
cert: './certs/cert.pem',
key: './certs/key.pem'
});
server.on('connection', conn => {
const h3 = attachHTTP3(conn);
h3.on('request', req => {
const chunks = [];
req.on('data', chunk => {
chunks.push(chunk);
});
req.on('end', () => {
const body = Buffer.concat(chunks);
req.respond(200, {
'content-type': 'application/octet-stream'
}, body);
});
});
h3.on('streamError', info => {
console.error('[HTTP/3 stream error]', info.error);
});
});
server.listen().catch(err => {
console.error(err);
process.exitCode = 1;
});HTTP/3 client
h3.request() returns a request object. The request exposes a response Promise that resolves after the complete response body has been received.
'use strict';
const fs = require('node:fs');
const { QUICClient, attachHTTP3 } = require('@xof/quic');
(async () => {
const client = new QUICClient({
host: '127.0.0.1',
port: 4433,
alpn: 'h3',
noPsk: true,
servername: 'localhost',
ca: fs.readFileSync('./certs/cert.pem'),
rejectUnauthorized: true,
handshakeTimeout: 15000
});
try {
const conn = await client.connect();
const h3 = attachHTTP3(conn);
const req = h3.request({
method: 'POST',
scheme: 'https',
authority: 'localhost',
path: '/',
headers: {
'content-type': 'application/octet-stream'
},
body: Buffer.from('hello from HTTP/3')
});
const res = await req.response;
console.log('status:', res.status);
console.log('headers:', res.headers);
console.log('trailers:', res.trailers);
console.log('body:', res.body);
console.log('bodyBytes:', res.bodyBytes);
console.log('text:', res.text());
} finally {
client.close();
}
})();HTTP/3 request methods
The current request() options are:
h3.request({
method: 'GET',
scheme: 'https',
authority: 'localhost',
path: '/',
headers: {},
body: null
});For requests with a body, the value can be a Buffer or another value accepted by Buffer.from().
The returned request object provides:
req.id;
req.response;
req.write(data);
req.end(data);
req.cancel();
req.on(event, handler);The response Promise resolves to an object containing:
{
id,
status,
headers,
trailers,
body, // Buffer
bodyBytes,
text(encoding),
json()
}HTTP/3 connections also emit ready, settings, goaway, headers, data, trailers, end, unknownFrame, streamError, and close events according to the received protocol state.
HTTP/3 response
On the server, the request object is a readable stream with HTTP/3 request metadata:
h3.on('request', req => {
console.log(req.method);
console.log(req.path);
console.log(req.authority);
console.log(req.scheme);
console.log(req.headers);
req.on('data', chunk => {
console.log(chunk);
});
req.on('end', () => {
req.respond(200, {
'content-type': 'application/json'
}, JSON.stringify({
creator: 'Xyraa Kyxzz',
status: true,
data: {
message: 'Hello from HTTP/3'
}
}));
});
});respond(status, headers, body) sends the response HEADERS and optional DATA, then finishes the stream.
Certificate mode
QUICServer enters certificate mode when noPsk: true or a certificate is configured.
const { QUICServer } = require('@xof/quic');
const server = new QUICServer({
host: '0.0.0.0',
port: 4433,
alpn: 'h3',
noPsk: true,
cert: './certs/cert.pem',
key: './certs/key.pem'
});The client-side certificate settings are provided through ca, servername, and rejectUnauthorized.
const client = new QUICClient({
host: '127.0.0.1',
port: 4433,
alpn: 'h3',
noPsk: true,
servername: 'localhost',
ca: fs.readFileSync('./certs/cert.pem'),
rejectUnauthorized: true
});For the bundled development certificate, servername: 'localhost' matches the certificate subject.
Handshake timeout and diagnostics
Both client and server use an explicit handshake timeout. The default is 15000 milliseconds and can be changed with handshakeTimeout.
const client = new QUICClient({
host: 'example.com',
port: 443,
handshakeTimeout: 10000
});
client.on('timeout', err => {
console.error(err.code);
console.error(err.diagnosis);
});DNS failures are reported separately from handshake timeouts. A timeout can include TCP reachability information in error.diagnosis, but TCP reachability does not prove that QUIC or HTTP/3 is available.
Connection diagnostics are also available after connection setup:
console.log(conn.getDiagnostics());
console.log(conn.getFingerprint());
console.log(conn.getPath());
console.log(conn.getPeerTransportParameters());QLog and packet diagnostics
QLog can be enabled from the connection or through the qlog server/client option:
conn.enableQLog();Packet-level forensic details are disabled by default. Enable them explicitly when detailed packet inspection is required:
XOF_PACKET_DETAILS=1 node your-script.jsThe repository also contains quic-detail.txt used by the diagnostic test script.
Repository server
The repository root contains a complete HTTP/3 development server in server.js. It uses the bundled development certificate and listens on UDP port 4433.
node server.jsOr with Node watch mode:
npm run devA matching HTTP/3 client can use the certificate-mode example shown above.
Examples
The repository also contains standalone client/server example files under examples/. The current HTTP/3 implementation itself is exposed through attachHTTP3() as shown in this README; a raw QUIC connection does not emit HTTP request/response objects by itself.
Testing
The package test script points to the diagnostic runner and requires a target host. Pass the host and optional port after --:
npm test -- example.com 443The same diagnostic script at the repository root can be run directly with a target host and optional port:
node test.js <host> [port]Example:
node test.js example.com 443It writes packet/TLS diagnostic output to quic-detail.txt.
Public API
The CommonJS entry point exports the protocol primitives from lib/index.js, including:
QUICClientQUICServerQUICConnectionHTTP3ConnectionattachHTTP3- QUIC v1/v2 constants and packet/frame helpers
- TLS 1.3, packet protection, HKDF, Retry, and QPACK helpers
- recovery, congestion-control, flow-control, path, token, and QLog helpers
QUICError,QUICTransportError,QUICProtocolError, andQUICCryptoError
const xquic = require('@xof/quic');
console.log(Object.keys(xquic));