{"record":{"id":"cb9995c1d11d74ab","repo":"mastra-ai/mastra","slug":"thread-not-found-threadid-cb9995","errorCode":null,"errorMessage":"Thread not found: ${threadId}","messagePattern":"Thread not found: (.+?)","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"packages/server/src/server/handlers/agent-controller.ts","lineNumber":1217,"sourceCode":"  description: 'Lists messages for a specific thread. Returns most recent messages first.',\n  tags: ['AgentController', 'Threads'],\n  requiresAuth: true,\n  requiresPermission: 'agent-controller:read',\n  handler: async ({ mastra, controllerId, resourceId, threadId, limit }) => {\n    try {\n      const controller = getAgentControllerOrThrow(mastra, controllerId);\n      // Read-only route: query storage directly instead of constructing a\n      // Session. Session creation would trigger workspace/sandbox\n      // initialization as a side effect; reads should never pay that cost.\n      // The query methods lazily initialize storage (not workspace) on their own.\n      // The route is authorized for the URL's resourceId, but `threadId` is\n      // otherwise unscoped. Verify the thread belongs to this resource so a\n      // caller can't peek at another resource's messages by guessing an id\n      // — matches the check `session.thread.listMessages` performed via\n      // `session.thread.set` before we bypassed session construction.\n      const thread = await controller.queryThreadById({ threadId });\n      if (!thread || thread.resourceId !== resourceId) {\n        throw new Error(`Thread not found: ${threadId}`);\n      }\n      const messages = await controller.queryThreadMessages({ threadId, limit });\n      return {\n        messages: messages.map(m => ({\n          id: m.id,\n          role: m.role,\n          content: m.content as { format: 2; parts: Array<{ type: string; [key: string]: unknown }> },\n          createdAt: m.createdAt instanceof Date ? m.createdAt.toISOString() : undefined,\n          threadId: m.threadId,\n          resourceId: m.resourceId,\n          type: m.type,\n        })),\n      };\n    } catch (error) {\n      return handleError(error, 'error listing controller thread messages');\n    }\n  },\n});","sourceCodeStart":1199,"sourceCodeEnd":1235,"githubUrl":"https://github.com/mastra-ai/mastra/blob/75dd419e613fe9c39f846ffc500716141b74fda6/packages/server/src/server/handlers/agent-controller.ts#L1199-L1235","documentation":"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.","triggerScenarios":"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.","commonSituations":"Multi-tenant apps passing one tenant's threadId with another's resourceId; shared database across deployments with divergent resourceIds; stale ids after wiping storage.","solutions":["Send the resourceId that actually owns the thread — verify ownership mapping in your client.","Confirm the threadId is valid in the current storage backend before calling.","If the thread was deleted/migrated, recreate it or point the client at the new id.","Check that resourceId normalization (case, prefixes) matches what was used at thread creation."],"exampleFix":"// before\nconst msgs = await api.listMessages({ resourceId: 'user-a', threadId: threadOwnedByUserB });\n// after\nconst msgs = await api.listMessages({ resourceId: 'user-b', threadId: threadOwnedByUserB });","handlingStrategy":"validation","validationCode":"const thread = await api.getThread({ threadId });\nif (!thread || thread.resourceId !== resourceId) {\n  throw new Error('Refusing to list messages: thread missing or not owned by this resource');\n}","typeGuard":"function isOwnedThread(t: { resourceId: string } | null | undefined, resourceId: string): t is { resourceId: string } {\n  return !!t && t.resourceId === resourceId;\n}","tryCatchPattern":"try {\n  return await api.listMessages({ resourceId, threadId });\n} catch (e) {\n  if (e instanceof Error && e.message.startsWith('Thread not found')) {\n    return { messages: [] }; // treat as empty/no-access rather than crashing\n  }\n  throw e;\n}","preventionTips":["Keep a tenant/resource → thread-id mapping in your app so ids never cross resources.","Check ownership client-side before calls in multi-tenant UIs.","Log and alert on mismatches — they usually indicate an IDOR attempt or bad keying."],"tags":["ownership","thread","multi-tenant","authorization"],"backgroundTag":"thread-not-found","analyzedSha":"75dd419e613fe9c39f846ffc500716141b74fda6","analyzedAt":"2026-08-30T00:15:31.844Z","schemaVersion":2},"datasetVersion":"2026-08-30T03:17:51.788Z"}