moeru-ai/airi · error · InvalidMessageError

Invalid AIRI websocket message.

Error message

Invalid AIRI websocket message.

What it means

This is the client-side decode path in server-sdk/src/codec.ts. Incoming text is first parsed with SuperJSON; if that yields an object with a `type` field it is used, otherwise parsePlainJson is tried. InvalidMessageError ('Invalid AIRI websocket message.') is thrown when the parsed object reached safeParse(eventEnvelopeSchema, ...) and failed, i.e. the text was parseable but is not a valid AIRI event envelope (missing/invalid `type`, wrong payload shape).

Solutions

  1. Confirm the peer serializes with the matching stringifyEvent from the same protocol version
  2. Read the valibot issues on the error's `cause` to find the offending envelope field
  3. Pin server and client packages to compatible versions and redeploy together
  4. When mocking a server in tests, emit events built from the shared event definitions instead of ad-hoc objects

Example fix

// before
server.clients.forEach(c => c.send(JSON.stringify({ ping: Date.now() })))
// after
import { stringifyEvent } from '@proj-airi/server-runtime'
server.clients.forEach(c => c.send(stringifyEvent({ type: 'ping', payload: { at: Date.now() } })))
Defensive patterns

Strategy: try-catch

Validate before calling

import { safeParse } from 'valibot'
import { eventEnvelopeSchema } from '@proj-airi/server-shared'

const check = safeParse(eventEnvelopeSchema, candidate)
if (!check.success)
  return { ok: false as const, issues: check.issues }

Type guard

function looksLikeWebSocketEvent(value: unknown): value is { type: string } {
  return typeof value === 'object' && value !== null && 'type' in value
    && typeof (value as { type: unknown }).type === 'string'
}

Try / catch

try {
  const event = parseEvent<C>(text)
}
catch (error) {
  if (error instanceof InvalidMessageError) {
    logger.warn('ignoring non-conforming message', { issues: error.cause, source: error.source })
    return
  }
  throw error
}

Prevention

When it happens

Trigger: The server (or a test double) sending plain objects without the envelope `type` field, events produced by an older/newer protocol version whose schema diverged, or SuperJSON payloads whose reconstructed types no longer match eventEnvelopeSchema.

Common situations: See trigger scenarios.

Related errors


AI-assisted analysis of moeru-ai/airi@677329427f (2026-08-18). Data as JSON: /api/errors/2eacd26411fc44c9. Report an issue: GitHub.

Appendix: source

Thrown at packages/server-sdk/src/codec.ts:58

/** Parses one AIRI websocket protocol event from SuperJSON or plain JSON text. */
export function parseEvent<C = undefined>(text: string): WebSocketEvent<C> {
  let superJsonParsed: WebSocketEvent<C> | undefined
  let superJsonError: unknown

  try {
    superJsonParsed = parse<WebSocketEvent<C>>(text)
  }
  catch (error) {
    superJsonError = error
  }

  const potentialEvent = superJsonParsed && typeof superJsonParsed === 'object' && 'type' in superJsonParsed
    ? superJsonParsed
    : parsePlainJson(text, superJsonError)

  const result = safeParse(eventEnvelopeSchema, potentialEvent)
  if (!result.success) {
    throw new InvalidMessageError({ cause: result.issues, source: potentialEvent })
  }

  return potentialEvent as WebSocketEvent<C>
}

/** Serializes one AIRI websocket protocol event with SuperJSON. */
export function stringifyEvent<C = undefined>(
  event: WebSocketBaseEvent<string, unknown> | WebSocketEvent<C>,
) {
  return stringify(event)
}

function parsePlainJson(text: string, superJsonError: unknown): unknown {
  try {
    return JSON.parse(text)
  }
  catch (jsonError) {
    throw new InvalidMessageError({

View on GitHub (pinned to 677329427f)