{"record":{"id":"e9ccf17e749d6c90","repo":"moeru-ai/airi","slug":"chat-ws-dropped-malformed-newmessages-payload","errorCode":null,"errorMessage":"[chat-ws] dropped malformed newMessages payload:","messagePattern":"\\[chat-ws\\] dropped malformed newMessages payload:","errorType":"console","errorClass":null,"httpStatus":null,"severity":"warning","filePath":"packages/stage-ui/src/libs/chat-sync/ws-client.ts","lineNumber":264,"sourceCode":"  function disposeContext() {\n    while (contextDisposers.length > 0) {\n      const dispose = contextDisposers.pop()!\n      try {\n        dispose()\n      }\n      catch {}\n    }\n    context.value = undefined\n  }\n\n  function attachContextListeners(ctx: WsEventContext) {\n    contextDisposers.push(ctx.on(newMessages, (event) => {\n      // External boundary: validate the wire payload before fanning it out.\n      // A malformed server push would otherwise flow unchecked into every\n      // subscriber and into `mergeCloudMessagesIntoSession`.\n      const result = v.safeParse(NewMessagesPayloadSchema, event.body)\n      if (!result.success) {\n        console.warn('[chat-ws] dropped malformed newMessages payload:', result.issues[0]?.message)\n        return\n      }\n      const payload = result.output\n      for (const handler of newMessagesHandlers) {\n        try {\n          handler(payload)\n        }\n        catch (err) {\n          // Same isolation principle as notifyStatus: one bad listener should\n          // not silently drop messages for the rest.\n          console.warn('[chat-ws] newMessages handler threw:', errorMessageFrom(err))\n        }\n      }\n    }))\n\n    contextDisposers.push(ctx.on(wsErrorEvent, (event) => {\n      console.warn('[chat-ws] socket error:', event.body)\n    }))","sourceCodeStart":246,"sourceCodeEnd":282,"githubUrl":"https://github.com/moeru-ai/airi/blob/b6d0809ecb6421d492c4ce38aa7a76146c83269d/packages/stage-ui/src/libs/chat-sync/ws-client.ts#L246-L282","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Read result.issues[0].message in the warn and compare the received shape (log event.body temporarily) against NewMessagesPayloadSchema.","Align server payload generation and the Valibot schema in the same release; use optional()/nullish for fields the server may omit.","If a field is newly added, make the schema additive (optional first, required later) so old/new clients coexist.","Note the payload is dropped, not queued — fetch a resync from the server after fixing."],"exampleFix":"// before\nconst name: string = v.string() // server sends null on deleted senders\n\n// after\nconst name: string = v.nullable(v.string()) // or v.optional with a fallback","handlingStrategy":"validation","validationCode":"const result = v.safeParse(NewMessagesPayloadSchema, event.body)\nif (!result.success) {\n  // drop and optionally request a server resync\n  return\n}","typeGuard":"function isNewMessagesPayload(v: unknown): boolean {\n  return v.safeParse(NewMessagesPayloadSchema, v).success\n}","tryCatchPattern":null,"preventionTips":["Keep wire schemas in a shared package so server and client compile against one definition.","Make schema changes additive (optional fields first) during rolling deploys.","Pair validation drops with a resync fetch so no messages are permanently lost."],"tags":["websocket","valibot","schema","chat-sync","server-drift"],"backgroundTag":"schema-validation-failed","analyzedSha":"b6d0809ecb6421d492c4ce38aa7a76146c83269d","analyzedAt":"2026-08-18T17:29:58.153Z","schemaVersion":2},"datasetVersion":"2026-08-23T13:39:53.451Z"}