{"record":{"id":"fa862feff7ea7fb9","repo":"mastra-ai/mastra","slug":"access-denied-cannot-save-messages-for-a-differen","errorCode":null,"errorMessage":"Access denied: cannot save messages for a different resource","messagePattern":"Access denied: cannot save messages for a different resource","errorType":"http","errorClass":"HTTPException","httpStatus":403,"severity":"error","filePath":"packages/server/src/server/handlers/memory.ts","lineNumber":1341,"sourceCode":"          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({\n            mastra,\n            requestContext,\n            threadId,\n            thread,\n            effectiveResourceId,\n            permission: MastraFGAPermissions.MEMORY_WRITE,\n          });\n        }\n      } else {","sourceCodeStart":1323,"sourceCodeEnd":1359,"githubUrl":"https://github.com/mastra-ai/mastra/blob/75dd419e613fe9c39f846ffc500716141b74fda6/packages/server/src/server/handlers/memory.ts#L1323-L1359","documentation":"When the server resolves an effective resourceId (from request context, e.g. authenticated user or agent-scoped resource), all messages in a save-messages batch must belong to that resource. Any message whose resourceId differs from the effective one triggers HTTP 403, blocking cross-resource data pollution and unauthorized writes.","triggerScenarios":"POST /memory/save-messages made in a context where effectiveResourceId is set (request context / auth provides it) while one or more messages carry a different `resourceId`, e.g. saving another user's messages while impersonating/locked to your own resource.","commonSituations":"Multi-tenant apps where a user token pins resourceId but client code sends a hardcoded or stale resourceId; admin tooling replaying another user's history without overriding context; copy-pasted test payloads with someone else's resourceId.","solutions":["Set every message's `resourceId` to the resource bound to your request context (the authenticated/effective one).","Drop messages belonging to other resources from the batch before sending.","If you legitimately need to write for another resource, use credentials/context authorized for that resource (e.g., admin impersonation path) rather than mixing ids.","Check for stale cached resourceId in the client after a login/user switch and refresh it."],"exampleFix":"// before\nawait client.saveMessages({ messages: msgs.map(m => ({ ...m, resourceId: 'user-b' })) }); // effectiveResourceId is 'user-a'\n// after\nawait client.saveMessages({ messages: msgs.map(m => ({ ...m, resourceId: effectiveResourceId })) });","handlingStrategy":"validation","validationCode":"const unauthorized = messages.filter(m => effectiveResourceId && m.resourceId !== effectiveResourceId);\nif (unauthorized.length) {\n  throw new Error(`${unauthorized.length} message(s) belong to a different resource`);\n}","typeGuard":null,"tryCatchPattern":"try {\n  await saveMessages({ messages });\n} catch (e) {\n  if (isHttpError(e) && e.status === 403 && e.message.includes('different resource')) {\n    // drop foreign-resource messages or re-authenticate as the owning resource\n  } else throw e;\n}","preventionTips":["Always derive message resourceId from the authenticated user/resource context, never hardcode it.","Refresh cached resourceId on login/user switch.","For admin/cross-resource tooling, use explicitly authorized impersonation credentials.","Filter batches to the current resource before sending."],"tags":["http-403","authorization","multi-tenancy","memory"],"backgroundTag":"cross-resource-access-denied","analyzedSha":"75dd419e613fe9c39f846ffc500716141b74fda6","analyzedAt":"2026-08-30T00:15:31.844Z","schemaVersion":2},"datasetVersion":"2026-08-30T03:17:51.788Z"}