@xof/h3
v1.0.0
Published
Lightweight HTTP/3 application framework for Node.js built on @xof/quic.
Readme
@xof/h3
HTTP/3 application framework for Node.js, powered by @xof/quic.
CommonJS-first · native Node.js APIs · routing · middleware · body parsing · structured errors
Requirements
- Node.js
>= 20 - UDP access to the server port
@xof/quic1.0.0- A TLS certificate and private key for normal HTTP/3 certificate mode
@xof/h3 does not implement a second QUIC stack. It uses QUICServer and attachHTTP3() from @xof/quic, so HTTP/3 traffic stays on the same XQUIC transport and QPACK implementation.
Installation
npm install @xof/h3Quick Start
The application API intentionally follows @xof/app closely. The transport difference is that @xof/h3 listens on UDP and negotiates HTTP/3 with ALPN h3.
'use strict';
const xof = require('@xof/h3');
const app = xof({
host: '0.0.0.0',
port: 4433,
cert: './certs/cert.pem',
key: './certs/key.pem'
});
app.get('/', (req, res) => {
res.json({
status: true,
data: {
message: 'Hello HTTP/3'
}
});
});
app.start();Configuration
const app = xof({
port: 4433,
host: '0.0.0.0',
bodyLimit: '1mb',
notallow: true,
cert: './certs/cert.pem',
key: './certs/key.pem',
certChain: undefined,
handshakeTimeout: 15000,
requireRetry: false,
qlog: false
});port defaults to 4433 for local HTTP/3 development. bodyLimit defaults to 1mb. notallow defaults to false.
All other XQUIC server options are passed through to @xof/quic, except the HTTP framework controls port, host, bodyLimit, notallow, and forces ALPN to h3.
Routing
app.get('/users', handler);
app.post('/users', handler);
app.put('/users/:id', handler);
app.patch('/users/:id', handler);
app.delete('/users/:id', handler);
app.head('/users', handler);
app.options('/users', handler);
app.connect('/tunnel', handler);
app.all('/health', handler);Route parameters are available through req.params:
app.get('/users/:id', (req, res) => {
res.json({ id: req.params.id });
});Wildcard routes are supported:
app.get('/assets/*', (req, res) => {
res.send(req.params['*']);
});Query Parameters
req.query is an object. Repeated keys become arrays.
app.get('/search', (req, res) => {
console.log(req.query.q);
console.log(req.query.tag);
res.json({
status: true,
data: req.query
});
});For /search?q=quic&tag=h3&tag=http3, req.query.tag is ['h3', 'http3'].
Middleware
Global middleware is chainable and supports synchronous or asynchronous functions.
app.use(async (req, res, next, ctx) => {
console.log(`${req.method} ${req.path}`);
await next();
});
app.get('/private', authenticate, handler);The context follows the same shape used by @xof/app:
ctx.req;
ctx.res;
ctx.route;
ctx.params;
ctx.error;
ctx.set('user', user);
ctx.get('user');
ctx.has('user');
ctx.delete('user');
ctx.elapsed();Request Body
Non-multipart bodies are buffered before route middleware runs.
Supported formats:
application/jsonapplication/*+jsontext/*application/x-www-form-urlencoded- other content types as
Buffer
app.post('/users', (req, res) => {
console.log(req.body);
console.log(req.rawBody);
res.json({ status: true, data: req.body });
});URL-encoded input uses native URLSearchParams:
const username = req.body.get('username');multipart/form-data is not consumed by the built-in parser, matching the @xof/app behavior so a dedicated multipart parser can read the request stream itself.
A body larger than bodyLimit produces HTTP 413 with ERR_BODY_TOO_LARGE.
Response
The response API mirrors the common @xof/app helpers:
res.status(201);
res.statusCode = 201;
res.set('x-server', '@xof/h3');
res.header('x-test', 'ok');
res.get('content-type');
res.remove('x-test');
res.type('application/json');
res.json({ status: true });
res.send('hello');
res.write('hello');
res.end();
res.flushHeaders();
res.redirect('/login');
res.redirect(301, '/new-location');
res.writeHead(200, { 'cache-control': 'no-store' });Buffer and Uint8Array responses are supported directly. write() / flushHeaders() use XQUIC's HTTP/3 stream framing, so a response can be sent incrementally.
For HEAD, 204, and 304, response bodies are not transmitted while Content-Length is preserved when applicable.
Errors
When no handler matches a request, the framework returns a structured JSON error.
{
"status": false,
"data": {
"message": "Cannot GET /unknown"
}
}With notallow: true, an existing path requested with an unsupported method returns 405 and an Allow header.
Custom error middleware uses the same four leading parameters as @xof/app plus the request context:
app.use((error, req, res, next, ctx) => {
console.error(error);
if (res.writableEnded) return;
res.status(error.status || 500).json({
status: false,
data: {
message: error.status ? error.message : 'Internal Server Error'
}
});
});If no custom error middleware handles an error, the framework returns a protected 500 Internal Server Error body.
Lifecycle
app.start();
app.listen(4433);
app.listen(4433, '0.0.0.0');
app.close();Callbacks follow the same general pattern:
app.start(server => {
console.log('HTTP/3 started');
});
app.close(() => {
console.log('HTTP/3 closed');
});app.server is the underlying XQUIC QUICServer instance, not a Node http.Server.
XQUIC Alignment
The framework deliberately relies on these XQUIC primitives:
const { QUICServer, attachHTTP3 } = require('@xof/quic');The HTTP/3 connection layer is therefore the one already implemented by XQUIC, including QUIC v1/v2 transport support, TLS 1.3, streams, QPACK, HTTP/3 SETTINGS, GOAWAY, trailers, request cancellation, and transport diagnostics.
For certificate mode, keep the XQUIC server configuration in the same form:
const app = xof({
host: '0.0.0.0',
port: 4433,
noPsk: true,
cert: './certs/cert.pem',
key: './certs/key.pem'
});The framework always uses ALPN h3.
Testing with curl
With a local certificate, use the same UDP port as the XQUIC HTTP/3 server:
curl --http3-only -k https://127.0.0.1:4433/For a public certificate:
curl --http3-only https://example.com/The server must be reachable over UDP on the target port.
Package API
const xof = require('@xof/h3');Exports:
xof(options)H3ApplicationRouterContextH3ResponseparseLimit()parseRequestBody()METHODS
Notes
@xof/h3 is a framework layer, not a replacement for the QUIC implementation. Keep @xof/quic and @xof/h3 on compatible releases; this package currently pins @xof/quic to 1.0.0.
