mastra-ai/mastra · error · HTTPException
Access denied: thread belongs to a different resource
Error message
Access denied: thread belongs to a different resource
What it means
validateThreadOwnership throws this 403 when a thread's resourceId does not match the effective resource ID derived from the request (body memory.resource or authenticated user). It prevents a user from reading, signaling, subscribing to, or aborting another user's conversation thread.
Source
Thrown at packages/server/src/server/handlers/utils.ts:121
export function getEffectiveThreadId(
requestContext: RequestContext | undefined,
clientThreadId: string | undefined,
): string | undefined {
const contextThreadId = requestContext?.get(MASTRA_THREAD_ID_KEY) as string | undefined;
return contextThreadId || clientThreadId;
}
/**
* Validates that a thread belongs to the specified resourceId.
* Throws 403 if the thread exists but belongs to a different resource.
* Threads with no resourceId are accessible to all (shared threads).
*/
export async function validateThreadOwnership(
thread: { resourceId?: string | null } | null | undefined,
effectiveResourceId: string | undefined,
): Promise<void> {
if (thread && effectiveResourceId && thread.resourceId && thread.resourceId !== effectiveResourceId) {
throw new HTTPException(403, { message: 'Access denied: thread belongs to a different resource' });
}
}
/**
* Validates both coarse resource ownership and fine-grained thread access.
* FGA enforcement is a no-op when no FGA provider is configured.
*/
export async function enforceThreadAccess({
mastra,
requestContext,
threadId,
thread,
effectiveResourceId,
permission = MastraFGAPermissions.MEMORY_READ,
}: {
mastra: any;
requestContext?: RequestContext;
threadId: string;View on GitHub (pinned to 75dd419e61)
Solutions
- Use the same memory.resource (or authenticated identity) that originally created the thread.
- Create a new thread scoped to the current resource instead of reusing a foreign threadId.
- Check storage for the thread's resourceId and align your requests with it.
- Fix mapUserToResourceId or body memory.resource inconsistencies across environments.
Example fix
// before
const res = await fetch(`/api/agents/assistant/thread/${threadId}/signal`, { method: 'POST', body: JSON.stringify({ memory: { resource: 'user-b' }, ... }) }); // thread belongs to user-a
// after
const res = await fetch(`/api/agents/assistant/thread/${threadId}/signal`, { method: 'POST', body: JSON.stringify({ memory: { resource: 'user-a' }, ... }) }); // or use a thread created for user-b Defensive patterns
Strategy: try-catch
Validate before calling
async function assertThreadOwnable(threadId: string, resourceId: string, fetchThread: (id: string) => Promise<{ resourceId?: string | null } | null>) {
const thread = await fetchThread(threadId);
if (thread?.resourceId && thread.resourceId !== resourceId) {
throw new Error(`Thread ${threadId} belongs to resource '${thread.resourceId}', not '${resourceId}'`);
}
} Type guard
function threadBelongsToResource(thread: { resourceId?: string | null } | null, resourceId: string): boolean {
return !!thread && (!thread.resourceId || thread.resourceId === resourceId);
} Try / catch
try {
const res = await fetch(`/api/agents/assistant/thread/${threadId}/signal`, { method: 'POST', body: JSON.stringify(payload) });
if (res.status === 403) throw new Error(`Thread ${threadId} is owned by a different resource; use a thread created for this resource`);
return await res.json();
} catch (e) { throw e; } Prevention
- Persist the (resourceId, threadId) pair together and always send the matching memory.resource.
- Never share threadIds across users or environments.
- Verify thread resourceId in storage when debugging access issues.
- Keep mapUserToResourceId deterministic across deploys.
When it happens
Trigger: Calling SEND_AGENT_SIGNAL, agent message, abort-thread, subscribe-thread, or stream-until-idle routes with a threadId whose stored resourceId differs from the caller's effectiveResourceId.
Common situations: Reusing a threadId from another user/test account; switching memory.resource between requests on the same thread; multiple devs sharing a dev server with different resource IDs; seeded threads with stale resourceId values after changing the resource mapping.
Understand the failure class
- Authentication and authorization failures — expired tokens, bad credentials, and missing scopes.
Related errors
- Access denied: cannot save messages for 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/e5795a4f86f1483a.
Report an issue: GitHub.