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

  1. On the client, always send via the SDK's `stringifyEvent(event)` so the wire format matches the server's SuperJSON-based parse path.
  2. Ensure every emitted event is a valid envelope: an object with a string `type` and a non-array object `data`.
  3. Filter heartbeat frames (ping/pong) upstream using `heartbeatFrameFrom(text)` before calling `parseEvent`.
  4. 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

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


AI-assisted analysis of moeru-ai/airi@27111382b4 (2026-08-12). Data as JSON: /api/errors/606be38246248543. Report an issue: GitHub.