mastra-ai/mastra · error · HTTPException
Access denied: cannot save messages for a different resource
Error message
Access denied: cannot save messages for a different resource
What it means
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.
Source
Thrown at packages/server/src/server/handlers/memory.ts:1341
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({
mastra,
requestContext,
threadId,
thread,
effectiveResourceId,
permission: MastraFGAPermissions.MEMORY_WRITE,
});
}
} else {View on GitHub (pinned to 75dd419e61)
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.
Example fix
// before
await client.saveMessages({ messages: msgs.map(m => ({ ...m, resourceId: 'user-b' })) }); // effectiveResourceId is 'user-a'
// after
await client.saveMessages({ messages: msgs.map(m => ({ ...m, resourceId: effectiveResourceId })) }); Defensive patterns
Strategy: validation
Validate before calling
const unauthorized = messages.filter(m => effectiveResourceId && m.resourceId !== effectiveResourceId);
if (unauthorized.length) {
throw new Error(`${unauthorized.length} message(s) belong to a different resource`);
} Try / catch
try {
await saveMessages({ messages });
} catch (e) {
if (isHttpError(e) && e.status === 403 && e.message.includes('different resource')) {
// drop foreign-resource messages or re-authenticate as the owning resource
} else throw e;
} Prevention
- 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.
When it happens
Trigger: 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.
Common situations: 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.
Understand the failure class
- Authentication and authorization failures — expired tokens, bad credentials, and missing scopes.
Related errors
- Access denied: thread belongs to a different resource
- AGENT_MEMORY_THREAD_RESOURCE_MISMATCH
- Thread with id ${threadId} is for resource with id ${thread.
- Thread not found
- Thread does not belong to the active resource
AI-assisted analysis of mastra-ai/mastra@75dd419e61 (2026-08-30).
Data as JSON: /api/errors/fa862feff7ea7fb9.
Report an issue: GitHub.