{"record":{"id":"f9f710705a702d10","repo":"nocobase/nocobase","slug":"messages-must-be-an-array","errorCode":null,"errorMessage":"messages must be an array","messagePattern":"messages must be an array","errorType":"validation","errorClass":"ResourceActionError","httpStatus":400,"severity":"error","filePath":"packages/plugins/@nocobase/plugin-ai/src/server/resource/aiConversations.ts","lineNumber":371,"sourceCode":"        messages,\n        editingMessageId,\n        model,\n        webSearch,\n        stream = true,\n      } = ctx.action.params.values || {};\n\n      const shouldStream = stream !== false;\n      if (shouldStream) {\n        setupSSEHeaders(ctx);\n      }\n\n      try {\n        if (!sessionId) {\n          throw new ResourceActionError(400, ctx.t('sessionId is required'));\n        }\n\n        if (!Array.isArray(messages)) {\n          throw new ResourceActionError(400, ctx.t('messages must be an array'));\n        }\n        normalizeIncomingMessageAttachments(ctx, messages);\n        const userMessage = messages.find((message: any) => message.role === 'user');\n        if (!userMessage) {\n          throw new ResourceActionError(400, ctx.t('user message is required'));\n        }\n\n        const conversation = await plugin.aiConversationsManager.getConversation({\n          sessionId,\n          userId,\n        });\n\n        if (!conversation) {\n          throw new ResourceActionError(400, ctx.t('conversation not found'));\n        }\n\n        const employee = await getAIEmployee(ctx, employeeName);\n        if (!employee) {","sourceCodeStart":353,"sourceCodeEnd":389,"githubUrl":"https://github.com/nocobase/nocobase/blob/fa42722fefe44265490dff2c27d79e2882bce4fa/packages/plugins/@nocobase/plugin-ai/src/server/resource/aiConversations.ts#L353-L389","documentation":"sendMessages validates that the incoming messages field is an array before reading message roles. A 400 ResourceActionError is thrown when messages is missing, null, or any non-array value (string, object, number). This guards downstream code that calls messages.find and iterates the list.","triggerScenarios":"Calling sendMessages with values.messages undefined, with messages as a single object instead of an array (e.g. { messages: { role: 'user', ... } }), or with a JSON body where messages was serialized as null.","commonSituations":"Client accidentally passes one message object rather than wrapping it in an array; an SDK/HTTP layer drops the field because of a wrong content-type so it deserializes to undefined; refactored client sends 'message' (singular) key instead of 'messages'.","solutions":["Wrap the message payload in an array: messages: [{ role: 'user', content: ... }]","Verify the request key is exactly 'messages' (not 'message') and is included in the serialized body","If a single message, convert: const msgs = Array.isArray(m) ? m : [m]","Check content-type is application/json so the body parses into an array rather than undefined"],"exampleFix":"// before\nsendMessages({ values: { sessionId, messages: { role: 'user', content: { type: 'text', content: 'hi' } } } })\n// after\nsendMessages({ values: { sessionId, messages: [{ role: 'user', content: { type: 'text', content: 'hi' } }] } })","handlingStrategy":"validation","validationCode":"function assertMessagesArray(messages) {\n  if (!Array.isArray(messages)) throw new TypeError('messages must be an array of message objects');\n  if (messages.length === 0) throw new TypeError('messages must not be empty');\n}","typeGuard":"function isMessageArray(v) {\n  return Array.isArray(v) && v.every((m) => m !== null && typeof m === 'object' && typeof m.role === 'string');\n}","tryCatchPattern":"try {\n  await sendMessages({ values: { sessionId, messages } });\n} catch (err) {\n  if (err?.status === 400 && /messages must be an array/.test(err.message ?? '')) {\n    return sendMessages({ values: { sessionId, messages: [messages] } }); // wrap single object\n  }\n  throw err;\n}","preventionTips":["Always wrap single messages in an array before the request","Keep the key name exactly 'messages' in payloads and configs","Validate payload shape client-side (zod/ajv) before calling the action","Check the HTTP request content-type is application/json so arrays deserialize correctly"],"tags":["validation","bad-request","request-shape","ai-conversations"],"backgroundTag":"schema-validation-failed","analyzedSha":"fa42722fefe44265490dff2c27d79e2882bce4fa","analyzedAt":"2026-09-01T00:54:31.202Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-08T05:18:18.240Z"}