branch:
http-websockets.md
21298 bytesRaw
# HTTP & WebSockets
Agents handle both HTTP requests and WebSocket connections, giving you flexibility to build REST APIs, real-time applications, or hybrid architectures.
## Overview
Every agent can respond to:
- **HTTP requests** via `onRequest()` - REST APIs, webhooks, file uploads
- **WebSocket connections** via `onConnect()`, `onMessage()`, `onClose()` - Real-time bidirectional communication
```typescript
import { Agent } from "agents";
export class MyAgent extends Agent {
// Handle HTTP requests
onRequest(request: Request): Response {
return new Response("Hello from HTTP!");
}
// Handle WebSocket connections
onConnect(connection: Connection, ctx: ConnectionContext) {
connection.send("Welcome!");
}
onMessage(connection: Connection, message: WSMessage) {
// Echo back
connection.send(message);
}
}
```
## Lifecycle Hooks
Agents have several lifecycle hooks that are called at different points:
| Hook | When Called |
| --------------------------------------------- | --------------------------------------------------------- |
| `onStart(props?)` | Once when the agent first starts (before any connections) |
| `onRequest(request)` | When an HTTP request is received (non-WebSocket) |
| `onConnect(connection, ctx)` | When a new WebSocket connection is established |
| `onMessage(connection, message)` | When a WebSocket message is received |
| `onClose(connection, code, reason, wasClean)` | When a WebSocket connection closes |
| `onError(connection, error)` | When a WebSocket error occurs |
| `onError(error)` | When a server error occurs (overloaded) |
### Lifecycle Flow
```
Agent Created
↓
onStart() ←── Called once, before any connections
↓
┌─────────────────────────────────────┐
│ For each request: │
│ │
│ HTTP Request ──→ onRequest() │
│ │
│ WebSocket ──→ onConnect() │
│ ↓ │
│ Messages ──→ onMessage() (repeat) │
│ ↓ │
│ Disconnect ──→ onClose() │
└─────────────────────────────────────┘
```
## HTTP Requests
Handle HTTP requests with `onRequest()`. This is called for any non-WebSocket request to your agent.
```typescript
export class ApiAgent extends Agent {
async onRequest(request: Request): Promise<Response> {
const url = new URL(request.url);
// Route by path
if (url.pathname.endsWith("/status")) {
return Response.json({
status: "ok",
connections: this.getConnections().length
});
}
if (url.pathname.endsWith("/data") && request.method === "POST") {
const data = await request.json();
// Process data...
return Response.json({ received: true });
}
return new Response("Not found", { status: 404 });
}
}
```
### Common HTTP Patterns
**REST API with CORS:**
```typescript
onRequest(request: Request): Response {
// Handle preflight
if (request.method === "OPTIONS") {
return new Response(null, {
headers: {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Methods": "GET, POST, OPTIONS",
"Access-Control-Allow-Headers": "Content-Type"
}
});
}
return Response.json(
{ data: "..." },
{ headers: { "Access-Control-Allow-Origin": "*" } }
);
}
```
**File Upload:**
```typescript
async onRequest(request: Request): Promise<Response> {
if (request.method === "POST") {
const formData = await request.formData();
const file = formData.get("file") as File;
// Process file...
const content = await file.text();
return Response.json({ filename: file.name, size: file.size });
}
return new Response("Method not allowed", { status: 405 });
}
```
## WebSocket Connections
WebSockets enable real-time bidirectional communication between clients and your agent.
### Connection Lifecycle
```typescript
export class ChatAgent extends Agent {
// Called when a client connects
onConnect(connection: Connection, ctx: ConnectionContext) {
// ctx.request contains the original HTTP request (for auth, headers, etc.)
const url = new URL(ctx.request.url);
const token = url.searchParams.get("token");
console.log(`Client ${connection.id} connected`);
connection.send(JSON.stringify({ type: "welcome", id: connection.id }));
}
// Called for each message from this connection
onMessage(connection: Connection, message: WSMessage) {
if (typeof message === "string") {
const data = JSON.parse(message);
// Handle message...
} else {
// Binary message (ArrayBuffer)
}
}
// Called when connection closes
onClose(
connection: Connection,
code: number,
reason: string,
wasClean: boolean
) {
console.log(`Client ${connection.id} disconnected: ${code} ${reason}`);
}
// Called on WebSocket errors
onError(connection: Connection, error: unknown) {
console.error(`Error on connection ${connection.id}:`, error);
}
}
```
### The Connection Object
Each WebSocket connection is represented by a `Connection` object:
```typescript
interface Connection<TState = unknown> {
/** Unique connection identifier */
id: string;
/** The agent instance name this connection belongs to */
server: string;
/** Per-connection state (read-only, use setState to update) */
state: TState | null;
/** Update connection state */
setState(state: TState | ((prev: TState | null) => TState)): void;
/** Send a message to this connection */
send(message: string | ArrayBuffer): void;
/** Close this connection */
close(code?: number, reason?: string): void;
}
```
### Message Types
Messages can be strings or binary:
```typescript
onMessage(connection: Connection, message: WSMessage) {
if (typeof message === "string") {
// Text message - usually JSON
const data = JSON.parse(message);
this.handleTextMessage(connection, data);
} else {
// Binary message - ArrayBuffer or ArrayBufferView
this.handleBinaryMessage(connection, message);
}
}
```
## Connection Management
### Getting Connections
```typescript
// Get all connections
const connections = this.getConnections();
// Get a specific connection by ID
const connection = this.getConnection("abc123");
// Get connections with a specific tag
const adminConnections = this.getConnections("admin");
```
### Broadcasting
Send a message to all connected clients:
```typescript
// Broadcast to everyone
this.broadcast(JSON.stringify({ type: "update", data: "..." }));
// Broadcast to everyone except specific connections
this.broadcast(
JSON.stringify({ type: "user-typing", userId: "123" }),
["connection-id-to-exclude"] // Don't send to the originator
);
```
### Connection Tags
Tag connections for easy filtering. Override `getConnectionTags()` to assign tags:
```typescript
export class ChatAgent extends Agent {
// Called when a connection is established
getConnectionTags(connection: Connection, ctx: ConnectionContext): string[] {
const url = new URL(ctx.request.url);
const role = url.searchParams.get("role");
const tags: string[] = [];
if (role === "admin") tags.push("admin");
if (role === "moderator") tags.push("moderator");
return tags; // Up to 9 tags, max 256 chars each
}
// Later, broadcast only to admins
notifyAdmins(message: string) {
for (const conn of this.getConnections("admin")) {
conn.send(message);
}
}
}
```
## Per-Connection State
Store data specific to each connection using `connection.state` and `connection.setState()`:
```typescript
type ConnectionState = {
username: string;
joinedAt: number;
messageCount: number;
};
export class ChatAgent extends Agent {
onConnect(connection: Connection<ConnectionState>, ctx: ConnectionContext) {
const url = new URL(ctx.request.url);
// Initialize connection state
connection.setState({
username: url.searchParams.get("username") || "Anonymous",
joinedAt: Date.now(),
messageCount: 0
});
}
onMessage(connection: Connection<ConnectionState>, message: WSMessage) {
// Update message count using functional update
connection.setState((prev) => ({
...prev!,
messageCount: (prev?.messageCount || 0) + 1
}));
// Access current state
const { username, messageCount } = connection.state!;
console.log(`${username} sent message #${messageCount}`);
}
}
```
**Important:** Connection state is:
- **Immutable** - Read via `connection.state`, update via `connection.setState()`
- **Per-connection** - Each connection has its own state
- **Persisted across hibernation** - Survives agent sleep/wake cycles
## The `onStart` Hook
`onStart()` is called once when the agent first starts, before any connections are established:
```typescript
export class MyAgent extends Agent {
private cache: Map<string, unknown> = new Map();
async onStart() {
// Initialize resources
console.log(`Agent ${this.name} starting...`);
// Load data from storage
const savedData = this.sql`SELECT * FROM cache`;
for (const row of savedData) {
this.cache.set(row.key, row.value);
}
// Restore MCP connections, check workflows, etc.
// (Agent does this automatically, but you can add custom logic)
}
onConnect(connection: Connection) {
// By the time connections arrive, onStart has completed
}
}
```
## Protocol Message Control
By default, when a WebSocket client connects, the agent sends protocol text frames (`CF_AGENT_IDENTITY`, `CF_AGENT_STATE`, `CF_AGENT_MCP_SERVERS`) to keep the client in sync. You can suppress these on a per-connection basis by overriding `shouldSendProtocolMessages`:
```typescript
export class MyAgent extends Agent {
shouldSendProtocolMessages(
connection: Connection,
ctx: ConnectionContext
): boolean {
// Suppress protocol frames for binary-only clients
const url = new URL(ctx.request.url);
return url.searchParams.get("protocol") !== "false";
}
}
```
When `shouldSendProtocolMessages` returns `false` for a connection:
- No `CF_AGENT_IDENTITY`, `CF_AGENT_STATE`, or `CF_AGENT_MCP_SERVERS` frames are sent on connect
- The connection is excluded from protocol broadcasts (state updates, MCP server changes)
- Regular messages via `connection.send()` and `this.broadcast()` still work normally
This is useful for IoT devices, binary-only clients, or lightweight consumers that only need raw messages.
### Checking Protocol Status
Use `isConnectionProtocolEnabled` to check whether a connection receives protocol messages:
```typescript
const enabled = this.isConnectionProtocolEnabled(connection);
```
This status persists across hibernation — a connection that was marked as no-protocol before hibernation remains no-protocol after waking up.
## Error Handling
Handle errors gracefully with `onError`:
```typescript
export class MyAgent extends Agent {
// WebSocket connection error
onError(connection: Connection, error: unknown): void {
console.error(`WebSocket error on ${connection.id}:`, error);
connection.send(
JSON.stringify({ type: "error", message: "Connection error" })
);
}
// Server error (overloaded signature - no connection parameter)
onError(error: unknown): void {
console.error("Server error:", error);
// Log to external service, etc.
}
}
```
## Hibernation
Agents support hibernation - they can sleep when inactive and wake when needed. This saves resources while maintaining WebSocket connections.
### Enabling Hibernation
Hibernation is enabled by default. To disable:
```typescript
export class AlwaysOnAgent extends Agent {
static options = { hibernate: false };
}
```
### How Hibernation Works
1. Agent is active, handling connections
2. After ~10 seconds of no messages, agent hibernates (sleeps)
3. WebSocket connections remain open (handled by Cloudflare)
4. When a message arrives, agent wakes up
5. `onMessage` is called as normal
### What Persists Across Hibernation
| Persists | Does Not Persist |
| -------------------------- | ------------------- |
| `this.state` (agent state) | In-memory variables |
| `connection.state` | Timers/intervals |
| SQLite data (`this.sql`) | Promises in flight |
| Connection metadata | Local caches |
**Best Practice:** Store important data in `this.state` or SQLite, not in class properties:
```typescript
export class MyAgent extends Agent<Env, { counter: number }> {
initialState = { counter: 0 };
// ❌ Don't do this - lost on hibernation
private localCounter = 0;
onMessage(connection: Connection, message: WSMessage) {
// ✅ Do this - persists
this.setState({ counter: this.state.counter + 1 });
// ❌ Lost after hibernation
this.localCounter++;
}
}
```
## Common Patterns
### Authentication on Connect
Validate users when they connect:
```typescript
export class SecureAgent extends Agent {
async onConnect(connection: Connection, ctx: ConnectionContext) {
const url = new URL(ctx.request.url);
const token = url.searchParams.get("token");
if (!token || !(await this.validateToken(token))) {
connection.close(4001, "Unauthorized");
return;
}
const user = await this.getUserFromToken(token);
connection.setState({ userId: user.id, role: user.role });
connection.send(JSON.stringify({ type: "authenticated", user }));
}
private async validateToken(token: string): Promise<boolean> {
// Validate JWT, check database, etc.
return true;
}
}
```
### Chat Room with Broadcast
```typescript
type Message = {
type: "message" | "join" | "leave";
user: string;
text?: string;
timestamp: number;
};
export class ChatRoom extends Agent {
onConnect(connection: Connection, ctx: ConnectionContext) {
const url = new URL(ctx.request.url);
const username = url.searchParams.get("username") || "Anonymous";
connection.setState({ username });
// Notify others
this.broadcast(
JSON.stringify({
type: "join",
user: username,
timestamp: Date.now()
} satisfies Message),
[connection.id] // Don't send to the joining user
);
}
onMessage(connection: Connection, message: WSMessage) {
if (typeof message !== "string") return;
const { username } = connection.state as { username: string };
// Broadcast to everyone
this.broadcast(
JSON.stringify({
type: "message",
user: username,
text: message,
timestamp: Date.now()
} satisfies Message)
);
}
onClose(connection: Connection) {
const { username } = (connection.state as { username: string }) || {};
if (username) {
this.broadcast(
JSON.stringify({
type: "leave",
user: username,
timestamp: Date.now()
} satisfies Message)
);
}
}
}
```
### Presence Tracking
Track who's online using per-connection state. This pattern is clean because connection state is automatically cleaned up when users disconnect:
```typescript
type UserState = {
name: string;
joinedAt: number;
lastSeen: number;
};
export class PresenceAgent extends Agent {
onConnect(connection: Connection<UserState>, ctx: ConnectionContext) {
const url = new URL(ctx.request.url);
const name = url.searchParams.get("name") || "Anonymous";
// Store user data on the connection itself
connection.setState({
name,
joinedAt: Date.now(),
lastSeen: Date.now()
});
// Send current presence to new user
connection.send(
JSON.stringify({
type: "presence",
users: this.getPresence()
})
);
// Notify others that someone joined
this.broadcastPresence();
}
onClose(connection: Connection) {
// No manual cleanup needed - connection state is automatically gone
// Just broadcast updated presence to remaining users
this.broadcastPresence();
}
// Heartbeat to update lastSeen
onMessage(connection: Connection<UserState>, message: WSMessage) {
if (message === "ping") {
connection.setState((prev) => ({
...prev!,
lastSeen: Date.now()
}));
connection.send("pong");
}
}
// Build presence from all connections
private getPresence() {
const users: Record<string, { name: string; lastSeen: number }> = {};
for (const conn of this.getConnections<UserState>()) {
if (conn.state) {
users[conn.id] = {
name: conn.state.name,
lastSeen: conn.state.lastSeen
};
}
}
return users;
}
private broadcastPresence() {
this.broadcast(
JSON.stringify({
type: "presence",
users: this.getPresence()
})
);
}
}
```
## API Reference
### Agent Lifecycle Methods
| Method | Signature | Description |
| ---------------------------- | --------------------------------------------------------------- | ------------------------------------------------------ |
| `onStart` | `(props?) => void \| Promise<void>` | Called once when agent starts |
| `onRequest` | `(request: Request) => Response \| Promise<Response>` | Handle HTTP requests |
| `onConnect` | `(connection, ctx) => void \| Promise<void>` | WebSocket connected |
| `onMessage` | `(connection, message) => void \| Promise<void>` | Message received |
| `onClose` | `(connection, code, reason, wasClean) => void \| Promise<void>` | Connection closed |
| `onError` | `(connection, error) => void \| Promise<void>` | WebSocket error |
| `onError` | `(error) => void \| Promise<void>` | Server error (overload) |
| `shouldSendProtocolMessages` | `(connection, ctx) => boolean` | Control per-connection protocol frames (default: true) |
### Connection Management Methods
| Method | Signature | Description |
| ----------------------------- | ----------------------------------------- | ---------------------------------------------- |
| `getConnections` | `(tag?: string) => Iterable<Connection>` | Get all connections, optionally by tag |
| `getConnection` | `(id: string) => Connection \| undefined` | Get connection by ID |
| `getConnectionTags` | `(connection, ctx) => string[]` | Override to tag connections |
| `broadcast` | `(message, without?: string[]) => void` | Send to all connections |
| `isConnectionProtocolEnabled` | `(connection) => boolean` | Check if connection receives protocol messages |
### Connection Object
| Property/Method | Type | Description |
| --------------- | ------------------------------------------ | -------------------------------- |
| `id` | `string` | Unique connection identifier |
| `server` | `string` | Agent instance name |
| `state` | `T \| null` | Per-connection state (read-only) |
| `setState` | `(state \| (prev) => state) => void` | Update connection state |
| `send` | `(message: string \| ArrayBuffer) => void` | Send message |
| `close` | `(code?, reason?) => void` | Close connection |
### Agent Properties
| Property | Type | Description |
| ------------ | -------------------- | ----------------------------------- |
| `this.name` | `string` | Agent instance name |
| `this.state` | `State` | Agent state (use with `setState()`) |
| `this.env` | `Env` | Environment bindings |
| `this.ctx` | `DurableObjectState` | Durable Object context |