mastra-ai/mastra · error · HTTPException
All messages must have threadId and resourceId fields. Found
Error message
All messages must have threadId and resourceId fields. Found ${invalidMessages.length} invalid message(s). What it means
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.
Source
Thrown at packages/server/src/server/handlers/memory.ts:1332
const resourceIdByThread = new Map<string, string>();
for (const message of incomingMessages) {
if (!message.threadId || !message.resourceId) {
continue;
}
const existingResourceId = resourceIdByThread.get(message.threadId);
if (!existingResourceId) {
resourceIdByThread.set(message.threadId, message.resourceId);
} else if (existingResourceId !== message.resourceId) {
throw new HTTPException(400, {
message: 'All messages for the same threadId must use the same resourceId.',
});
}
}
// Validate that all messages have threadId and resourceId
const invalidMessages = incomingMessages.filter(message => !message.threadId || !message.resourceId);
if (invalidMessages.length > 0) {
throw new HTTPException(400, {
message: `All messages must have threadId and resourceId fields. Found ${invalidMessages.length} invalid message(s).`,
});
}
// If effectiveResourceId is set, validate all messages belong to this resource
if (effectiveResourceId) {
const unauthorizedMessages = incomingMessages.filter(message => message.resourceId !== effectiveResourceId);
if (unauthorizedMessages.length > 0) {
throw new HTTPException(403, {
message: 'Access denied: cannot save messages for a different resource',
});
}
// Validate that all threads belong to this resource (prevents cross-resource data pollution)
const threadIds = [...new Set(incomingMessages.map(m => m.threadId).filter(Boolean))] as string[];
for (const threadId of threadIds) {
const thread = await memory.getThreadById({ threadId });
await enforceThreadAccess({View on GitHub (pinned to 75dd419e61)
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.
Example fix
// before
const body = { messages: rawMessages.map(m => ({ role: m.role, content: m.content })) };
// after
const body = { messages: rawMessages.map(m => ({ ...m, threadId, resourceId })) }; Defensive patterns
Strategy: validation
Validate before calling
const invalid = messages.filter(m => !m.threadId || !m.resourceId);
if (invalid.length) {
throw new TypeError(`${invalid.length} message(s) missing threadId/resourceId`);
} Type guard
function isSavableMessage(m: unknown): m is { threadId: string; resourceId: string } & Record<string, unknown> {
const o = m as Record<string, unknown>;
return typeof o.threadId === 'string' && o.threadId.length > 0 && typeof o.resourceId === 'string' && o.resourceId.length > 0;
} Try / catch
try {
await saveMessages({ messages });
} catch (e) {
if (isHttpError(e) && e.status === 400 && e.message.includes('threadId and resourceId')) {
const count = Number(e.message.match(/Found (\d+)/)?.[1] ?? 0);
// drop or fix `count` invalid messages and retry
} else throw e;
} Prevention
- 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.
When it happens
Trigger: 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' }]`.
Common situations: 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.
Understand the failure class
Background: "Missing required field" and "field is required" errors: why libraries reject payloads that omit mandatory fields — this error's family across 20 libraries.
Related errors
- Messages should be an array
- Both threadId or resourceId must be provided
- Tool call id is required
- Agent "${agent.id}" does not have memory configured
- Memory storage is not configured for agent "${agent.id}"
AI-assisted analysis of mastra-ai/mastra@75dd419e61 (2026-08-30).
Data as JSON: /api/errors/5fc8588c06f96677.
Report an issue: GitHub.