mastra-ai/mastra · error · HTTPException
Access denied: unable to verify message ownership
Error message
Access denied: unable to verify message ownership
What it means
In DELETE_MESSAGES_ROUTE, when an effectiveResourceId is present the handler must verify that every message being deleted belongs to that resource. It resolves storage from memory?.storage or getStorageFromContext({ mastra }); if neither yields a storage adapter, it fails closed with HTTPException 403 'Access denied: unable to verify message ownership'. This is a deliberate security default, not a bug signal.
Source
Thrown at packages/server/src/server/handlers/memory.ts:1758
} else if (typeof messageIds === 'string') {
// Single string ID - wrap in array
normalizedIds = [messageIds];
} else {
// Single object with id property - wrap in array
normalizedIds = [messageIds];
}
// Extract string IDs for validation and deletion
const stringIds = normalizedIds.map(id => (typeof id === 'string' ? id : id.id));
const memory = await getMemoryFromContext({ mastra, agentId, requestContext, allowMissingAgent: true });
// If effectiveResourceId is set, validate ownership of all messages before deletion
// Fail closed: if we can't verify ownership, deny deletion
if (effectiveResourceId && stringIds.length > 0) {
const storage = memory?.storage || getStorageFromContext({ mastra });
if (!storage) {
throw new HTTPException(403, { message: 'Access denied: unable to verify message ownership' });
}
const memoryStore = await storage.getStore('memory');
if (!memoryStore) {
throw new HTTPException(400, { message: 'Memory is not initialized' });
}
await enforceDeleteMessagesThreadAccess({
mastra,
requestContext,
memoryStore,
messageIds: stringIds,
effectiveResourceId,
});
} else if (stringIds.length > 0) {
const storage = memory?.storage || getStorageFromContext({ mastra });
if (!storage) {
throw new HTTPException(400, { message: 'Memory is not initialized' });
}View on GitHub (pinned to 75dd419e61)
Solutions
- Configure storage on the Mastra instance: new Mastra({ storage: ... }) so ownership can be verified.
- Attach storage to the agent's Memory (new Memory({ storage })) so memory?.storage resolves.
- If you do not need resource-scoped deletion, omit resourceId so the ownership-check branch is skipped (access is then enforced via thread access instead).
- Check requestContext/resource overrides that inject an unintended effectiveResourceId.
Example fix
// before
new Mastra({ agents: { assistant } }); // no storage
// after
new Mastra({
agents: { assistant },
storage: new PgStorage({ connectionString: process.env.DATABASE_URL }),
}); Defensive patterns
Strategy: validation
Validate before calling
const storage = mastra.getStorage?.();
if (resourceId && !storage) {
throw new Error('resourceId-scoped message deletion requires Mastra-level storage for ownership checks');
} Type guard
function canVerifyOwnership(m: { storage?: unknown } | undefined, mastraStorage: unknown, resourceId?: string): boolean {
return !resourceId || !!(m?.storage ?? mastraStorage);
} Try / catch
try {
await client.deleteMessages(ids, { resourceId });
} catch (e) {
if (e instanceof HTTPException && e.status === 403) {
console.error('Cannot verify message ownership: configure storage or drop resourceId');
return;
}
throw e;
} Prevention
- In multi-tenant setups, always configure Mastra-level storage — ownership checks fail closed without it.
- Only pass resourceId when you actually need resource-scoped enforcement.
- Audit stored agents: unresolved memory + resourceId triggers this deny path.
When it happens
Trigger: POST /api/memory/messages/delete with resourceId set (or a requestContext resource override) while the Mastra instance has no storage configured and the resolved memory is undefined or has no .storage — ownership cannot be checked, so deletion is denied.
Common situations: Multi-tenant deployments passing resourceId for isolation but forgetting to configure storage on the server's Mastra instance; running with an in-memory-only setup in production; stored agents whose memory can't be resolved leaving memory undefined while resourceId is still supplied.
Understand the failure class
- Authentication and authorization failures — expired tokens, bad credentials, and missing scopes.
Related errors
- Access denied: durable run belongs to a different resource
- Access denied: cannot save messages for a different resource
- You do not have permission to disconnect this connection
- You do not have permission to view usage for this connection
- Access denied: thread belongs to a different resource
AI-assisted analysis of mastra-ai/mastra@75dd419e61 (2026-08-30).
Data as JSON: /api/errors/af0d97035da108ed.
Report an issue: GitHub.