{"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/677329427f32468c74b17f3ec47eeca4e05bec65/packages/server-runtime/src/server-ws/airi/codec.ts#L54-L82","documentation":"This is the decode path of the AIRI WebSocket server (server-runtime/src/server-ws/airi/codec.ts). Incoming text is parsed with SuperJSON; if that yields an object with a `type` field it is used directly, otherwise the text is re-parsed as plain JSON. The result must then pass safeParse against eventEnvelopeSchema; InvalidEventError ('Invalid WebSocket event format.') is thrown when parsing succeeded but the object does not satisfy the event envelope (typically a missing/ill-typed `type` or malformed payload).","triggerScenarios":"A client sending raw JSON that lacks the required envelope fields (e.g. `{ data: ... }` without `type`), a payload whose `type` is not a string, a SuperJSON-decoded object with unexpected extra shape, or a client/server version skew where eventEnvelopeSchema changed.","commonSituations":"Hand-rolled clients or test scripts that JSON.stringify arbitrary objects instead of using stringifyEvent, protocol drift after upgrading server-shared without rebuilding the client, or middleware/proxies rewriting frames.","solutions":["Send events through stringifyEvent / the shared codec so the SuperJSON envelope is correct","Inspect result.issues (attached as `cause`) to see exactly which envelope field failed","Align client and server on the same @proj-airi/server-shared / server-sdk versions and rebuild both","If testing manually, send at minimum `{ type: '<known event type>', payload: ... }` matching the schema"],"exampleFix":"// before\nws.send(JSON.stringify({ hello: 'world' }))\n// after\nimport { stringifyEvent } from '@proj-airi/server-sdk'\nws.send(stringifyEvent({ type: 'player-heartbeat-set', payload: { beatsPerMinute: 60 }, source: 'test' }))","handlingStrategy":"try-catch","validationCode":"import { safeParse } from 'valibot'\nimport { eventEnvelopeSchema } from '@proj-airi/server-shared'\n\nfunction isWireEventCandidate(text: string): boolean {\n  try {\n    const parsed: unknown = JSON.parse(text)\n    return safeParse(eventEnvelopeSchema, parsed).success\n  }\n  catch {\n    return false\n  }\n}","typeGuard":"function looksLikeWebSocketEvent(value: unknown): value is { type: string } {\n  return typeof value === 'object' && value !== null && 'type' in value\n    && typeof (value as { type: unknown }).type === 'string'\n}","tryCatchPattern":"try {\n  const event = parseEvent(text)\n}\ncatch (error) {\n  if (error instanceof InvalidEventError) {\n    // error.cause holds valibot issues, error.source the rejected payload\n    logger.warn('dropping malformed event', { issues: error.cause, source: error.source })\n    return // skip the frame; do not crash the socket handler\n  }\n  throw error\n}","preventionTips":["Always serialize with the shared stringifyEvent instead of JSON.stringify","Lock server and client protocol packages to the same version","Log error.cause (valibot issues) once to identify which envelope field drifts"],"tags":["websocket","superjson","valibot","schema","server-runtime"],"backgroundTag":"websocket-event-schema-mismatch","analyzedSha":"677329427f32468c74b17f3ec47eeca4e05bec65","analyzedAt":"2026-08-18T17:29:58.153Z","contentChangedAt":"2026-08-18T17:29:58.153Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}