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
- Confirm the peer serializes with the matching stringifyEvent from the same protocol version
- Read the valibot issues on the error's `cause` to find the offending envelope field
- Pin server and client packages to compatible versions and redeploy together
- 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
- Generate test/server traffic with stringifyEvent from the same version
- Treat protocol packages as one unit when upgrading the monorepo
- Check error.cause issues first; they name the exact failing field
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
- Invalid WebSocket event format.
- [chat-ws] dropped malformed newMessages payload:
- Client is not connected, current status
- The file is not an AIRI Live2D motion recording.
- Wrapped extension peer client does not support event…
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)