{"record":{"id":"5fc8588c06f96677","repo":"mastra-ai/mastra","slug":"all-messages-must-have-threadid-and-resourceid-fie","errorCode":null,"errorMessage":"All messages must have threadId and resourceId fields. Found ${invalidMessages.length} invalid message(s).","messagePattern":"All messages must have threadId and resourceId fields\\. Found (.+?) invalid message\\(s\\)\\.","errorType":"http","errorClass":"HTTPException","httpStatus":400,"severity":"error","filePath":"packages/server/src/server/handlers/memory.ts","lineNumber":1332,"sourceCode":"      const resourceIdByThread = new Map<string, string>();\n      for (const message of incomingMessages) {\n        if (!message.threadId || !message.resourceId) {\n          continue;\n        }\n        const existingResourceId = resourceIdByThread.get(message.threadId);\n        if (!existingResourceId) {\n          resourceIdByThread.set(message.threadId, message.resourceId);\n        } else if (existingResourceId !== message.resourceId) {\n          throw new HTTPException(400, {\n            message: 'All messages for the same threadId must use the same resourceId.',\n          });\n        }\n      }\n\n      // Validate that all messages have threadId and resourceId\n      const invalidMessages = incomingMessages.filter(message => !message.threadId || !message.resourceId);\n      if (invalidMessages.length > 0) {\n        throw new HTTPException(400, {\n          message: `All messages must have threadId and resourceId fields. Found ${invalidMessages.length} invalid message(s).`,\n        });\n      }\n\n      // If effectiveResourceId is set, validate all messages belong to this resource\n      if (effectiveResourceId) {\n        const unauthorizedMessages = incomingMessages.filter(message => message.resourceId !== effectiveResourceId);\n        if (unauthorizedMessages.length > 0) {\n          throw new HTTPException(403, {\n            message: 'Access denied: cannot save messages for a different resource',\n          });\n        }\n\n        // Validate that all threads belong to this resource (prevents cross-resource data pollution)\n        const threadIds = [...new Set(incomingMessages.map(m => m.threadId).filter(Boolean))] as string[];\n        for (const threadId of threadIds) {\n          const thread = await memory.getThreadById({ threadId });\n          await enforceThreadAccess({","sourceCodeStart":1314,"sourceCodeEnd":1350,"githubUrl":"https://github.com/mastra-ai/mastra/blob/75dd419e613fe9c39f846ffc500716141b74fda6/packages/server/src/server/handlers/memory.ts#L1314-L1350","documentation":"Every message saved via POST /memory/save-messages must include both `threadId` and `resourceId` fields. The handler filters messages missing either field and, if any exist, throws HTTP 400 reporting the count of invalid messages. Messages lacking these cannot be attributed to a thread/resource in storage.","triggerScenarios":"POST /memory/save-messages where one or more array entries omit `threadId` or `resourceId` (or set them to empty string/undefined), e.g. `messages: [{ content: 'hello' }]`.","commonSituations":"Passing raw model messages (which have only role/content) without enriching them with thread metadata; constructing messages manually for tests; a client refactor that renamed the metadata fields; saving partial results from a stream where context was lost.","solutions":["Add `threadId` and `resourceId` to every message object in the payload before sending.","Filter or fix invalid entries client-side first; the error message count tells you how many are bad.","If messages come from another system, map their fields to threadId/resourceId explicitly in an adapter.","Validate the whole batch with a schema (e.g., zod) client-side so misshapen messages never reach the endpoint."],"exampleFix":"// before\nconst body = { messages: rawMessages.map(m => ({ role: m.role, content: m.content })) };\n// after\nconst body = { messages: rawMessages.map(m => ({ ...m, threadId, resourceId })) };","handlingStrategy":"validation","validationCode":"const invalid = messages.filter(m => !m.threadId || !m.resourceId);\nif (invalid.length) {\n  throw new TypeError(`${invalid.length} message(s) missing threadId/resourceId`);\n}","typeGuard":"function isSavableMessage(m: unknown): m is { threadId: string; resourceId: string } & Record<string, unknown> {\n  const o = m as Record<string, unknown>;\n  return typeof o.threadId === 'string' && o.threadId.length > 0 && typeof o.resourceId === 'string' && o.resourceId.length > 0;\n}","tryCatchPattern":"try {\n  await saveMessages({ messages });\n} catch (e) {\n  if (isHttpError(e) && e.status === 400 && e.message.includes('threadId and resourceId')) {\n    const count = Number(e.message.match(/Found (\\d+)/)?.[1] ?? 0);\n    // drop or fix `count` invalid messages and retry\n  } else throw e;\n}","preventionTips":["Enrich raw model messages with threadId/resourceId before saving.","Run a schema check (zod) on the batch client-side.","Never construct save payloads from untyped `any` data.","Centralize message construction in one helper that always injects both fields."],"tags":["http-400","missing-field","memory","api"],"backgroundTag":"missing-required-field","analyzedSha":"75dd419e613fe9c39f846ffc500716141b74fda6","analyzedAt":"2026-08-30T00:15:31.844Z","schemaVersion":2},"datasetVersion":"2026-08-30T03:17:51.788Z"}