mirror of
https://github.com/oven-sh/bun
synced 2026-02-16 22:01:47 +00:00
345 lines
8.4 KiB
Markdown
345 lines
8.4 KiB
Markdown
# 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
|
|
|
|
1. **QuicSocket** - Represents a QUIC connection
|
|
2. **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
|
|
|
|
```javascript
|
|
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
|
|
|
|
```javascript
|
|
// 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
|
|
|
|
```javascript
|
|
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
|
|
|
|
```javascript
|
|
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
|
|
|
|
```javascript
|
|
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
|
|
|
|
1. **Client-initiated**:
|
|
- Client calls `socket.stream(data)`
|
|
- Stream ID assigned (0, 4, 8, 12...)
|
|
- `open(stream)` callback fires on both client and server
|
|
|
|
2. **Server-initiated**:
|
|
- Server calls `socket.stream(data)`
|
|
- Stream ID assigned (1, 5, 9, 13...)
|
|
- `open(stream)` callback fires on both sides
|
|
|
|
### Data Flow
|
|
|
|
1. Either side calls `stream.write(buffer)`
|
|
2. Other side receives `data(stream, buffer)` callback
|
|
3. Streams are bidirectional by default
|
|
|
|
### Stream Closure
|
|
|
|
1. `stream.end()` - Graceful closure (FIN)
|
|
2. `stream.close()` - Immediate closure (RESET)
|
|
3. `close(stream)` callback fires on both sides
|
|
|
|
## Important Notes
|
|
|
|
### No Direct Socket Writing
|
|
|
|
You cannot write directly to a QuicSocket:
|
|
```javascript
|
|
// ❌ 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:
|
|
```javascript
|
|
// ❌ 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
|
|
```javascript
|
|
error(socket, error) {
|
|
// Connection-level errors
|
|
// - TLS handshake failures
|
|
// - Network errors
|
|
// - Protocol violations
|
|
}
|
|
```
|
|
|
|
### Stream Errors
|
|
```javascript
|
|
stream: {
|
|
error(stream, error) {
|
|
// Stream-level errors
|
|
// - Stream reset by peer
|
|
// - Flow control violation
|
|
// - Stream-specific protocol errors
|
|
}
|
|
}
|
|
```
|
|
|
|
## Example: Echo Server
|
|
|
|
```javascript
|
|
// 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
|
|
|
|
```javascript
|
|
// 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. |