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

  1. Add `threadId` and `resourceId` to every message object in the payload before sending.
  2. Filter or fix invalid entries client-side first; the error message count tells you how many are bad.
  3. If messages come from another system, map their fields to threadId/resourceId explicitly in an adapter.
  4. 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

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


AI-assisted analysis of mastra-ai/mastra@75dd419e61 (2026-08-30). Data as JSON: /api/errors/5fc8588c06f96677. Report an issue: GitHub.