moeru-ai/airi · error · InvalidEventError
Invalid WebSocket event format.
Error message
Invalid WebSocket event format.
What it means
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).
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
Example fix
// before
ws.send(JSON.stringify({ hello: 'world' }))
// after
import { stringifyEvent } from '@proj-airi/server-sdk'
ws.send(stringifyEvent({ type: 'player-heartbeat-set', payload: { beatsPerMinute: 60 }, source: 'test' })) Defensive patterns
Strategy: try-catch
Validate before calling
import { safeParse } from 'valibot'
import { eventEnvelopeSchema } from '@proj-airi/server-shared'
function isWireEventCandidate(text: string): boolean {
try {
const parsed: unknown = JSON.parse(text)
return safeParse(eventEnvelopeSchema, parsed).success
}
catch {
return false
}
} 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(text)
}
catch (error) {
if (error instanceof InvalidEventError) {
// error.cause holds valibot issues, error.source the rejected payload
logger.warn('dropping malformed event', { issues: error.cause, source: error.source })
return // skip the frame; do not crash the socket handler
}
throw error
} Prevention
- 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
When it happens
Trigger: 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.
Common situations: 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.
Related errors
- Invalid AIRI websocket message.
- [chat-ws] dropped malformed newMessages payload:
- The file is not an AIRI Live2D motion recording.
- audio models upstream returned malformed body
- audio voices upstream returned malformed body
AI-assisted analysis of moeru-ai/airi@677329427f (2026-08-18).
Data as JSON: /api/errors/606be38246248543.
Report an issue: GitHub.
Appendix: 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 677329427f)