mirror of
https://github.com/oven-sh/bun
synced 2026-02-09 10:28:47 +00:00
8.4 KiB
8.4 KiB
QUIC API Design for Bun
Overview
Bun's QUIC implementation provides a pure QUIC API for low-level stream multiplexing over encrypted connections. This is separate from HTTP/3, which is built on top of QUIC but not covered here.
Core Concepts
Two Object Types
- QuicSocket - Represents a QUIC connection
- QuicStream - Represents an individual stream within a connection
Key Design Principles
- All callbacks passed upfront - Supports hot reloading by avoiding runtime callback assignment
- Stream-centric API - All data flows through streams, not the socket directly
- No HTTP/3 concepts - Pure QUIC only (no headers, no HTTP semantics)
Client API
Creating a Connection
const socket = await Bun.quic("example.com:443", {
// TLS configuration
tls: {
cert: Buffer, // Client certificate (optional)
key: Buffer, // Client private key (optional)
ca: Buffer, // CA certificate for verification
},
// Stream lifecycle callbacks (apply to ALL streams)
open(stream) {
// Called when a new stream is opened (by either side)
console.log("Stream opened:", stream.id);
console.log("Stream data:", stream.data); // Optional data attached to stream
},
data(stream, buffer) {
// Called when data is received on a stream
console.log("Received:", buffer);
stream.write(responseBuffer); // Can write back on same stream
},
drain(stream) {
// Called when a stream is writable again after backpressure
stream.write(moreData);
},
close(stream) {
// Called when a stream is closed
console.log("Stream closed:", stream.id);
},
error(stream, error) {
// Called on stream-level errors
console.error("Stream error:", error);
}
});
Creating Streams
// Create a new stream with optional associated data
const stream = socket.stream({
userId: 123,
requestId: "abc"
});
// The optional data becomes accessible via stream.data
console.log(stream.data); // { userId: 123, requestId: "abc" }
// Write data to the stream
stream.write(Buffer.from("Hello QUIC"));
// Close the stream when done
stream.end(); // or stream.close()
QuicSocket Methods
socket.stream(optionalData) // Create a new stream, returns QuicStream
socket.close() // Close the entire connection
socket.address // Remote address info
socket.localAddress // Local address info
QuicStream Properties & Methods
stream.write(buffer) // Write data to stream
stream.end() // Close stream gracefully
stream.close() // Close stream immediately
stream.data // Access optional data passed to socket.stream()
stream.id // Unique stream identifier
stream.socket // Reference to parent QuicSocket
Server API
Creating a Server
const server = Bun.listen({
port: 443,
hostname: "0.0.0.0",
// QUIC configuration
quic: {
cert: Buffer, // Server certificate (required)
key: Buffer, // Server private key (required)
ca: Buffer, // CA for client verification (optional)
passphrase: string, // Key passphrase (optional)
},
// Connection lifecycle (optional)
open(socket) {
// Called when a new QUIC connection is established
console.log("New connection from:", socket.address);
},
// Stream lifecycle callbacks (same as client)
stream: {
open(stream) {
// New stream opened by client
console.log("Client opened stream:", stream.id);
console.log("Stream data:", stream.data);
},
data(stream, buffer) {
// Data received from client
const request = buffer.toString();
// Echo back or process
stream.write(Buffer.from(`Echo: ${request}`));
// Server can also create new streams to the client
const pushStream = stream.socket.stream({ type: "push" });
pushStream.write(Buffer.from("Server-initiated data"));
},
drain(stream) {
// Stream writable again
},
close(stream) {
// Stream closed
},
error(stream, error) {
// Stream error
}
},
close(socket) {
// Connection closed
console.log("Connection closed");
},
error(socket, error) {
// Connection-level error
console.error("Connection error:", error);
}
});
// Stop the server
server.stop();
Stream Lifecycle
Stream Creation
-
Client-initiated:
- Client calls
socket.stream(data) - Stream ID assigned (0, 4, 8, 12...)
open(stream)callback fires on both client and server
- Client calls
-
Server-initiated:
- Server calls
socket.stream(data) - Stream ID assigned (1, 5, 9, 13...)
open(stream)callback fires on both sides
- Server calls
Data Flow
- Either side calls
stream.write(buffer) - Other side receives
data(stream, buffer)callback - Streams are bidirectional by default
Stream Closure
stream.end()- Graceful closure (FIN)stream.close()- Immediate closure (RESET)close(stream)callback fires on both sides
Important Notes
No Direct Socket Writing
You cannot write directly to a QuicSocket:
// ❌ WRONG - No socket.write() method
socket.write(data);
// ✅ CORRECT - Create a stream first
const stream = socket.stream();
stream.write(data);
All Callbacks Upfront
For hot reloading support, ALL callbacks must be passed in the initial options:
// ❌ WRONG - Cannot set callbacks after creation
const socket = await Bun.quic(url, {});
socket.onData = () => {}; // Not supported!
// ✅ CORRECT - Pass all callbacks upfront
const socket = await Bun.quic(url, {
data(stream, buffer) { ... },
open(stream) { ... }
});
Stream vs Connection Events
- Connection-level:
open(socket),close(socket),error(socket, error) - Stream-level:
stream.open(stream),stream.data(stream, buffer), etc. - Most events are stream-level since QUIC is stream-oriented
Pure QUIC, Not HTTP/3
This API is for pure QUIC only:
- No HTTP headers
- No request/response semantics
- No status codes
- Just bidirectional byte streams
HTTP/3 will be a separate API built on top of this.
Error Handling
Connection Errors
error(socket, error) {
// Connection-level errors
// - TLS handshake failures
// - Network errors
// - Protocol violations
}
Stream Errors
stream: {
error(stream, error) {
// Stream-level errors
// - Stream reset by peer
// - Flow control violation
// - Stream-specific protocol errors
}
}
Example: Echo Server
// Server
const server = Bun.listen({
port: 4433,
quic: { cert, key },
stream: {
data(stream, buffer) {
// Echo back on the same stream
stream.write(buffer);
}
}
});
// Client
const socket = await Bun.quic("localhost:4433", {
tls: { ca },
stream: {
data(stream, buffer) {
console.log("Received echo:", buffer.toString());
}
}
});
// Send data
const stream = socket.stream();
stream.write(Buffer.from("Hello QUIC!"));
Example: Multi-Stream Chat
// Client
const socket = await Bun.quic("chat.example.com:443", {
tls: { ca },
stream: {
open(stream) {
if (stream.data?.type === "notification") {
console.log("Server notification stream opened");
}
},
data(stream, buffer) {
const message = JSON.parse(buffer.toString());
if (stream.data?.type === "notification") {
console.log("Notification:", message);
} else {
console.log("Chat message:", message);
}
}
}
});
// Send a chat message
const chatStream = socket.stream({ type: "chat", room: "general" });
chatStream.write(JSON.stringify({
user: "alice",
message: "Hello everyone!"
}));
// Server can push notifications on a separate stream
// (in server code)
const notificationStream = socket.stream({ type: "notification" });
notificationStream.write(JSON.stringify({
event: "user_joined",
user: "bob"
}));
Implementation Status
⚠️ WARNING: As of now, this API design is documented but NOT IMPLEMENTED. The current implementation:
- Uses wrong callback structure (connection-level instead of stream-level)
- Lacks QuicStream objects
- Cannot actually transfer data between client and server
- Mixes HTTP/3 concepts with pure QUIC
See STATUS.md for current implementation state.