# 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 { 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 { 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 { /** 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, 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, 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 = 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 { 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 { // 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, 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, 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 = {}; for (const conn of this.getConnections()) { 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` | Called once when agent starts | | `onRequest` | `(request: Request) => Response \| Promise` | Handle HTTP requests | | `onConnect` | `(connection, ctx) => void \| Promise` | WebSocket connected | | `onMessage` | `(connection, message) => void \| Promise` | Message received | | `onClose` | `(connection, code, reason, wasClean) => void \| Promise` | Connection closed | | `onError` | `(connection, error) => void \| Promise` | WebSocket error | | `onError` | `(error) => void \| Promise` | Server error (overload) | | `shouldSendProtocolMessages` | `(connection, ctx) => boolean` | Control per-connection protocol frames (default: true) | ### Connection Management Methods | Method | Signature | Description | | ----------------------------- | ----------------------------------------- | ---------------------------------------------- | | `getConnections` | `(tag?: string) => Iterable` | 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 |