Files
bun.sh/API-DESIGN.md
Claude bff4d0d3e7 wip
2025-08-09 05:51:29 +02:00

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

  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

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

  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:

// ❌ 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.