moeru-ai/airi · error · InvalidEventError
Invalid WebSocket event format.
Error message
Invalid WebSocket event format.
What it means
Thrown as an `InvalidEventError` by `parseEvent()` in the server-runtime websocket codec when inbound text cannot be validated against the AIRI event envelope schema (`{ type: string, data: non-array object }`). The parser first tries SuperJSON, then plain JSON, then runs the valibot `safeParse`; a validation failure raises this error with the valibot issues as `cause` and the candidate object as `source`. The fixed message string is `'Invalid WebSocket event format.'`.
Source
Thrown at packages/server-runtime/src/server-ws/airi/codec.ts:72
// use superjson.parse instead of message.json() or plain JSON.parse first.
// JSON.parse on a superjson-encoded string returns the wrapper object
// `{ json: {...}, meta: {...} }` with no protocol `type`, which breaks routing.
// Keep this until all AIRI websocket clients share one non-wrapper wire format.
let parsed: WebSocketEvent | undefined
try {
parsed = parse<WebSocketEvent>(text)
}
catch {
parsed = undefined
}
const potentialEvent = (parsed && typeof parsed === 'object' && 'type' in parsed)
? parsed
: JSON.parse(text)
const result = safeParse(eventEnvelopeSchema, potentialEvent)
if (!result.success) {
throw new InvalidEventError({ cause: result.issues, source: potentialEvent })
}
return potentialEvent as WebSocketEvent
}
/** Serializes one AIRI websocket protocol event with the existing SuperJSON wire format. */
export function stringifyEvent(event: WebSocketBaseEvent<string, unknown> | string) {
return typeof event === 'string' ? event : stringify(event)
}
View on GitHub (pinned to 27111382b4)
Solutions
- On the client, always send via the SDK's `stringifyEvent(event)` so the wire format matches the server's SuperJSON-based parse path.
- Ensure every emitted event is a valid envelope: an object with a string `type` and a non-array object `data`.
- Filter heartbeat frames (ping/pong) upstream using `heartbeatFrameFrom(text)` before calling `parseEvent`.
- If integrating an external producer, align it on the AIRI event envelope schema before sending.
Example fix
// before (client)
ws.send(JSON.stringify({ type: 'chat', data: 'hi' })) // data must be an object
// after
ws.send(stringifyEvent({ type: 'chat', data: { message: 'hi' } })) Defensive patterns
Strategy: try-catch
Validate before calling
import { isInvalidEventError, parseEvent } from '@proj-airi/server-runtime/server-ws/airi/codec'
function tryParseEvent(text: string) {
try {
return parseEvent(text)
} catch (e) {
if (isInvalidEventError(e)) {
return { error: e, source: e.source, issues: e.cause }
}
throw e
}
} Type guard
import { isInvalidEventError } from '@proj-airi/server-runtime/server-ws/airi/codec'
function isAiriEnvelope(value: unknown): value is { type: string; data: object } {
return Boolean(value)
&& typeof value === 'object'
&& !Array.isArray(value)
&& typeof (value as any).type === 'string'
&& Boolean((value as any).data)
&& typeof (value as any).data === 'object'
&& !Array.isArray((value as any).data)
} Try / catch
try {
const event = parseEvent(text)
route(event)
} catch (e) {
if (isInvalidEventError(e)) {
log.warn('invalid websocket event', { source: e.source, issues: e.cause })
return // drop or close with protocol error
}
throw e
} Prevention
- Send only envelopes produced by stringifyEvent on the client side.
- Filter heartbeat ping/pong text frames with heartbeatFrameFrom before parseEvent.
- Ensure every event has a string type and a non-array object data field.
- Keep producer and consumer on the same protocol/schema version.
When it happens
Trigger: A websocket client sends a message that parses to JSON but is not a `{ type, data }` envelope — e.g. a bare string, an array, an object missing `type` or `data`, or `data` that is an array/primitive. Also triggered by a client using a plain `JSON.stringify` payload that lands as `{ json, meta }` (SuperJSON wrapper) without a protocol `type`, or by sending non-AIRI protocol messages on the AIRI socket.
Common situations: Misconfigured client that uses raw JSON instead of the SDK's `stringifyEvent`; a client on an incompatible protocol version; an external tool/probe hitting the AIRI websocket endpoint with arbitrary JSON; heartbeat ping/pong text frames not filtered upstream before reaching `parseEvent`.
Related errors
- Invalid AIRI websocket message.
- streaming-tts: not authenticated
- AIRI host websocket bridge is unavailable
- No candidate server channel URL was reachable. ${errors.join
- AIRI host websocket bridge is unavailable
AI-assisted analysis of moeru-ai/airi@27111382b4 (2026-08-12).
Data as JSON: /api/errors/606be38246248543.
Report an issue: GitHub.