Skip to navigation

WebSocket clients

View as Markdown
Enterprise feature

This feature is available only for the Enterprise plan. To get started, reach out to support@buildwithfern.com.

Fern generates typed WebSocket clients from your AsyncAPI channel definitions. The generated clients handle connection management, typed send/receive messages, and automatic reconnection.

Fern generates WebSocket clients for TypeScript, Python, Java, C#, Rust, Go, Ruby, PHP, and Swift.

Enable WebSocket client generation

Set generation.webSockets: true in sdk-config.yml. Set it in the top-level generation block to enable WebSocket clients for every target, or in a target’s generation block for one language. A target’s value overrides the top-level value:

sdk-config.yml
generation:
webSockets: true

Java generates WebSocket clients by default with no additional configuration; set webSockets: false to turn them off. Every other language generates them only when webSockets is true. With WebSocket generation off, the SDK still includes the message types.

Generated SDK behavior

Given an AsyncAPI channel definition like:

asyncapi.yml
asyncapi: 3.0.0
info:
title: Plant Monitoring Service
version: 1.0.0
channels:
plantUpdates:
address: /plants/{plant_id}/updates
parameters:
plant_id:
description: Plant identifier
messages:
SensorReading:
$ref: '#/components/messages/SensorReading'
PlantStatus:
$ref: '#/components/messages/PlantStatus'
operations:
sendReading:
action: send
channel:
$ref: '#/channels/plantUpdates'
messages:
- $ref: '#/channels/plantUpdates/messages/SensorReading'
receivePlantStatus:
action: receive
channel:
$ref: '#/channels/plantUpdates'
messages:
- $ref: '#/channels/plantUpdates/messages/PlantStatus'
components:
messages:
SensorReading:
payload:
type: object
properties:
temperature:
type: number
humidity:
type: number
PlantStatus:
payload:
type: object
properties:
health:
type: string
needsWater:
type: boolean

Fern generates the following client code.

Send methods are named send + the message name from the spec. The SensorReading message produces sendSensorReading in TypeScript, Java, Go, PHP, and Swift, and send_sensor_reading in Python, Ruby, and Rust. C# uses overloaded Send() methods instead.

Receive handling varies by language:

  • TypeScript — a generic socket.on("message", handler) callback
  • Python — typed responses from ws.recv()
  • Java — per-message-type handlers like onPlantStatus(handler)
  • C# — typed Event<T> properties like PlantStatus.Subscribe(handler)
  • Rust — a typed enum from ws.recv()
  • Go — per-message-type handlers like OnPlantStatus(handler), then Start()
  • Ruby — recv, each, or on(...) handlers with start_listening
  • PHP — iterate the socket with foreach, or call receive()
  • Swift — for await event in socket.events, with .message(.plantStatus(...))

The TypeScript SDK generates a typed socket class with send methods, event handlers, and a ReconnectingWebSocket that automatically retries dropped connections.

const client = new PlantClient({ token: "..." });
const socket = await client.plantUpdates.connect({
plant_id: "plant-123",
});
socket.on("open", () => {
console.log("Connected");
});
socket.on("message", (message) => {
// message is typed as PlantStatus
console.log(message.health);
});
socket.on("error", (error) => {
console.error(error);
});
await socket.waitForOpen();
socket.sendSensorReading({ temperature: 22.5, humidity: 65 });
socket.close();

Detach a handler with socket.off(event, handler). By default, registering a second handler for the same event replaces the first; set websocketHandlerMode: accumulate in generators.yml to fire every registered handler in order.

const logStatus = (message) => console.log(message.health);
socket.on("message", logStatus);
socket.off("message", logStatus);

The connection call accepts reconnectAttempts (default 30, 0 disables reconnection) and a shouldReconnect predicate that receives the CloseEvent when the server closes the connection and decides whether to reconnect. The default reconnects on every server close code except 1000. Return false to treat a server close as terminal, for example after the client has sent an end-of-stream message:

let streamEnded = false;
const socket = await client.plantUpdates.connect({
plant_id: "plant-123",
reconnectAttempts: 5,
shouldReconnect: (event) => !streamEnded && event.code !== 1000,
});
streamEnded = true;
await socket.waitForOpen();
socket.sendSensorReading({ temperature: 22.5, humidity: 65 });

shouldReconnect applies only to server-initiated closes. Connection errors and connection timeouts still fire close handlers but always retry up to reconnectAttempts, regardless of the predicate. shouldReconnect also isn’t consulted after socket.close() or when the abortSignal aborts, and a predicate that throws is treated as terminal. Calling socket.connect() after retries are exhausted starts a new connection attempt.

Authentication

In TypeScript, Python, Java, and Rust, WebSocket clients automatically inherit the authentication configured on the root client. Headers — including bearer tokens, API keys, and custom headers — are sent with the WebSocket handshake request. In TypeScript, the handshake carries the root client’s credentials when the auth scheme is declared under api.auth in sdk-config.yml.

const client = new PlantClient({
token: "your-bearer-token",
});
// Auth headers are forwarded to the WebSocket handshake
const socket = await client.plantUpdates.connect({
plant_id: "plant-123",
});

In C#, the WebSocket client created by the root client’s factory method sends the root client’s credentials and ClientOptions.AdditionalHeaders on the handshake and on every reconnect. Per connection, Options.AdditionalHeaders replaces a header of the same name, and a null value removes it.

Defining WebSocket channels

WebSocket clients are generated from channel definitions in your AsyncAPI specification. For defining channels, messages, and operations, see Publish/subscribe operations.