moeru-ai/airi · warning

[chat-ws] dropped malformed newMessages payload:

Error message

[chat-ws] dropped malformed newMessages payload:

What it means

The chat-sync WebSocket client validates every server 'newMessages' push against NewMessagesPayloadSchema with Valibot's safeParse before fanning out. On failure it logs the first issue's message, drops that one payload, and returns — one malformed server push cannot corrupt every subscriber or mergeCloudMessagesIntoSession. This is a trust-boundary guard: the payload never reaches handlers.

Source

Thrown at packages/stage-ui/src/libs/chat-sync/ws-client.ts:264

  function disposeContext() {
    while (contextDisposers.length > 0) {
      const dispose = contextDisposers.pop()!
      try {
        dispose()
      }
      catch {}
    }
    context.value = undefined
  }

  function attachContextListeners(ctx: WsEventContext) {
    contextDisposers.push(ctx.on(newMessages, (event) => {
      // External boundary: validate the wire payload before fanning it out.
      // A malformed server push would otherwise flow unchecked into every
      // subscriber and into `mergeCloudMessagesIntoSession`.
      const result = v.safeParse(NewMessagesPayloadSchema, event.body)
      if (!result.success) {
        console.warn('[chat-ws] dropped malformed newMessages payload:', result.issues[0]?.message)
        return
      }
      const payload = result.output
      for (const handler of newMessagesHandlers) {
        try {
          handler(payload)
        }
        catch (err) {
          // Same isolation principle as notifyStatus: one bad listener should
          // not silently drop messages for the rest.
          console.warn('[chat-ws] newMessages handler threw:', errorMessageFrom(err))
        }
      }
    }))

    contextDisposers.push(ctx.on(wsErrorEvent, (event) => {
      console.warn('[chat-ws] socket error:', event.body)
    }))

View on GitHub (pinned to b6d0809ecb)

Solutions

  1. Read result.issues[0].message in the warn and compare the received shape (log event.body temporarily) against NewMessagesPayloadSchema.
  2. Align server payload generation and the Valibot schema in the same release; use optional()/nullish for fields the server may omit.
  3. If a field is newly added, make the schema additive (optional first, required later) so old/new clients coexist.
  4. Note the payload is dropped, not queued — fetch a resync from the server after fixing.

Example fix

// before
const name: string = v.string() // server sends null on deleted senders

// after
const name: string = v.nullable(v.string()) // or v.optional with a fallback
Defensive patterns

Strategy: validation

Validate before calling

const result = v.safeParse(NewMessagesPayloadSchema, event.body)
if (!result.success) {
  // drop and optionally request a server resync
  return
}

Type guard

function isNewMessagesPayload(v: unknown): boolean {
  return v.safeParse(NewMessagesPayloadSchema, v).success
}

Prevention

When it happens

Trigger: Server and client schema drift (server adds/renames a field, changes a type, sends null for a required field); a server bug serializing Dates/undefined oddly; a proxy or serialization layer mangling the JSON body.

Common situations: Deploying client and server from different commits during a rolling release; changing the message wire contract without a versioned migration; server writing camelCase vs snake_case drift.

Understand the failure class

Background: Schema validation failed / invalid input schema: payload rejected because its shape doesn't match the expected schema — this error's family across 28 libraries.

Related errors


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