{"record":{"id":"606be38246248543","repo":"moeru-ai/airi","slug":"invalid-websocket-event-format","errorCode":null,"errorMessage":"Invalid WebSocket event format.","messagePattern":"Invalid WebSocket event format\\.","errorType":"validation","errorClass":"InvalidEventError","httpStatus":null,"severity":"error","filePath":"packages/server-runtime/src/server-ws/airi/codec.ts","lineNumber":72,"sourceCode":"  // use superjson.parse instead of message.json() or plain JSON.parse first.\n  // JSON.parse on a superjson-encoded string returns the wrapper object\n  // `{ json: {...}, meta: {...} }` with no protocol `type`, which breaks routing.\n  // Keep this until all AIRI websocket clients share one non-wrapper wire format.\n  let parsed: WebSocketEvent | undefined\n  try {\n    parsed = parse<WebSocketEvent>(text)\n  }\n  catch {\n    parsed = undefined\n  }\n\n  const potentialEvent = (parsed && typeof parsed === 'object' && 'type' in parsed)\n    ? parsed\n    : JSON.parse(text)\n\n  const result = safeParse(eventEnvelopeSchema, potentialEvent)\n  if (!result.success) {\n    throw new InvalidEventError({ cause: result.issues, source: potentialEvent })\n  }\n\n  return potentialEvent as WebSocketEvent\n}\n\n/** Serializes one AIRI websocket protocol event with the existing SuperJSON wire format. */\nexport function stringifyEvent(event: WebSocketBaseEvent<string, unknown> | string) {\n  return typeof event === 'string' ? event : stringify(event)\n}\n","sourceCodeStart":54,"sourceCodeEnd":82,"githubUrl":"https://github.com/moeru-ai/airi/blob/27111382b4a79a7e983289d6e983a06af185ed0f/packages/server-runtime/src/server-ws/airi/codec.ts#L54-L82","documentation":"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.'`.","triggerScenarios":"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.","commonSituations":"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`.","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."],"exampleFix":"// before (client)\nws.send(JSON.stringify({ type: 'chat', data: 'hi' })) // data must be an object\n\n// after\nws.send(stringifyEvent({ type: 'chat', data: { message: 'hi' } }))","handlingStrategy":"try-catch","validationCode":"import { isInvalidEventError, parseEvent } from '@proj-airi/server-runtime/server-ws/airi/codec'\n\nfunction tryParseEvent(text: string) {\n  try {\n    return parseEvent(text)\n  } catch (e) {\n    if (isInvalidEventError(e)) {\n      return { error: e, source: e.source, issues: e.cause }\n    }\n    throw e\n  }\n}","typeGuard":"import { isInvalidEventError } from '@proj-airi/server-runtime/server-ws/airi/codec'\n\nfunction isAiriEnvelope(value: unknown): value is { type: string; data: object } {\n  return Boolean(value)\n    && typeof value === 'object'\n    && !Array.isArray(value)\n    && typeof (value as any).type === 'string'\n    && Boolean((value as any).data)\n    && typeof (value as any).data === 'object'\n    && !Array.isArray((value as any).data)\n}","tryCatchPattern":"try {\n  const event = parseEvent(text)\n  route(event)\n} catch (e) {\n  if (isInvalidEventError(e)) {\n    log.warn('invalid websocket event', { source: e.source, issues: e.cause })\n    return // drop or close with protocol error\n  }\n  throw e\n}","preventionTips":["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."],"tags":["server-runtime","websocket","codec","validation","superjson"],"backgroundTag":null,"analyzedSha":"27111382b4a79a7e983289d6e983a06af185ed0f","analyzedAt":"2026-08-12T18:33:34.132Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}