mastra-ai/mastra · error · Error

Thread not found: ${threadId}

Error message

Thread not found: ${threadId}

What it means

The messages-listing path that bypasses full session construction verifies ownership by querying the thread and comparing its resourceId. It throws a plain Error (surfaced as 500 by handleError) when the thread is missing OR exists but belongs to a different resource — an anti-IDOR guard so callers cannot read other resources' messages by guessing ids.

Source

Thrown at packages/server/src/server/handlers/agent-controller.ts:1217

  description: 'Lists messages for a specific thread. Returns most recent messages first.',
  tags: ['AgentController', 'Threads'],
  requiresAuth: true,
  requiresPermission: 'agent-controller:read',
  handler: async ({ mastra, controllerId, resourceId, threadId, limit }) => {
    try {
      const controller = getAgentControllerOrThrow(mastra, controllerId);
      // Read-only route: query storage directly instead of constructing a
      // Session. Session creation would trigger workspace/sandbox
      // initialization as a side effect; reads should never pay that cost.
      // The query methods lazily initialize storage (not workspace) on their own.
      // The route is authorized for the URL's resourceId, but `threadId` is
      // otherwise unscoped. Verify the thread belongs to this resource so a
      // caller can't peek at another resource's messages by guessing an id
      // — matches the check `session.thread.listMessages` performed via
      // `session.thread.set` before we bypassed session construction.
      const thread = await controller.queryThreadById({ threadId });
      if (!thread || thread.resourceId !== resourceId) {
        throw new Error(`Thread not found: ${threadId}`);
      }
      const messages = await controller.queryThreadMessages({ threadId, limit });
      return {
        messages: messages.map(m => ({
          id: m.id,
          role: m.role,
          content: m.content as { format: 2; parts: Array<{ type: string; [key: string]: unknown }> },
          createdAt: m.createdAt instanceof Date ? m.createdAt.toISOString() : undefined,
          threadId: m.threadId,
          resourceId: m.resourceId,
          type: m.type,
        })),
      };
    } catch (error) {
      return handleError(error, 'error listing controller thread messages');
    }
  },
});

View on GitHub (pinned to 75dd419e61)

Solutions

  1. Send the resourceId that actually owns the thread — verify ownership mapping in your client.
  2. Confirm the threadId is valid in the current storage backend before calling.
  3. If the thread was deleted/migrated, recreate it or point the client at the new id.
  4. Check that resourceId normalization (case, prefixes) matches what was used at thread creation.

Example fix

// before
const msgs = await api.listMessages({ resourceId: 'user-a', threadId: threadOwnedByUserB });
// after
const msgs = await api.listMessages({ resourceId: 'user-b', threadId: threadOwnedByUserB });
Defensive patterns

Strategy: validation

Validate before calling

const thread = await api.getThread({ threadId });
if (!thread || thread.resourceId !== resourceId) {
  throw new Error('Refusing to list messages: thread missing or not owned by this resource');
}

Type guard

function isOwnedThread(t: { resourceId: string } | null | undefined, resourceId: string): t is { resourceId: string } {
  return !!t && t.resourceId === resourceId;
}

Try / catch

try {
  return await api.listMessages({ resourceId, threadId });
} catch (e) {
  if (e instanceof Error && e.message.startsWith('Thread not found')) {
    return { messages: [] }; // treat as empty/no-access rather than crashing
  }
  throw e;
}

Prevention

When it happens

Trigger: GET messages for a threadId that doesn't exist, or that exists under a different resourceId than the one authenticated/supplied; reusing a thread id copied from another user/project; storage migration losing the thread row.

Common situations: Multi-tenant apps passing one tenant's threadId with another's resourceId; shared database across deployments with divergent resourceIds; stale ids after wiping storage.

Related errors


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