mastra-ai/mastra · error · HTTPException

Access denied: unable to verify message thread access

Error message

Access denied: unable to verify message thread access

What it means

Thrown by `enforceDeleteMessagesThreadAccess` when at least one of the requested message ids either does not exist or exists but has no `threadId`, so thread-level access cannot be verified. The library refuses to delete messages whose owning thread it cannot determine, treating it as access-denied (403) rather than deleting blindly.

Source

Thrown at packages/server/src/server/handlers/memory.ts:169

async function enforceDeleteMessagesThreadAccess({
  mastra,
  requestContext,
  memoryStore,
  messageIds,
  effectiveResourceId,
}: {
  mastra: any;
  requestContext?: RequestContext;
  memoryStore: MemoryStorage;
  messageIds: string[];
  effectiveResourceId?: string;
}): Promise<void> {
  const { messages } = await memoryStore.listMessagesById({ messageIds });
  const threadIds = [...new Set(messages.map(m => m.threadId).filter(Boolean))] as string[];

  if (messages.some(message => !message.threadId)) {
    throw new HTTPException(403, { message: 'Access denied: unable to verify message thread access' });
  }

  for (const threadId of threadIds) {
    const thread = await memoryStore.getThreadById({ threadId });
    if (!thread) {
      throw new HTTPException(403, { message: 'Access denied: unable to verify message thread access' });
    }

    await enforceThreadAccess({
      mastra,
      requestContext,
      threadId,
      thread,
      effectiveResourceId,
      permission: MastraFGAPermissions.MEMORY_DELETE,
    });
  }
}

View on GitHub (pinned to 75dd419e61)

Solutions

  1. Re-fetch the thread's current messages and delete only ids that still exist in the list.
  2. Filter out messages with no `threadId` before sending the delete request, and handle detached messages via a separate flow.
  3. Verify you are using the same resourceId/storage scope the messages were created under.
  4. Make the delete idempotent client-side: tolerate 403 for already-deleted ids and retry only the remaining ids.

Example fix

// before
await api.deleteMessages({ messageIds: allIds })
// after
const current = await api.getMessages(threadId);
const deletable = allIds.filter(id => current.messages.some(m => m.id === id && m.threadId));
await api.deleteMessages({ messageIds: deletable });
Defensive patterns

Strategy: validation

Validate before calling

const { messages } = await api.listThreadMessages(threadId);
const deletable = requestedIds.filter(id => messages.some(m => m.id === id && m.threadId));
if (deletable.length !== requestedIds.length) {
  console.warn('Skipping ids not found or without thread:', requestedIds.filter(id => !deletable.includes(id)));
}

Type guard

function isDeletable(m: { id: string; threadId?: string | null }): m is { id: string; threadId: string } {
  return typeof m.threadId === 'string' && m.threadId.length > 0;
}

Try / catch

try {
  await api.deleteMessages({ messageIds });
} catch (e) {
  if (isHttpError(e, 403) && e.message.includes('thread access')) {
    // refetch current messages and retry with only valid ids
  }
  throw e;
}

Prevention

When it happens

Trigger: DELETE /api/memory/messages (DELETE_MESSAGES_ROUTE) with a body of `messageIds` where one or more ids are stale/already deleted, belong to another tenant, or were stored without a threadId (e.g. detached/unthreaded messages).

Common situations: See trigger scenarios.

Understand the failure class

Related errors


AI-assisted analysis of mastra-ai/mastra@75dd419e61 (2026-08-30). Data as JSON: /api/errors/5472b86bb1b67f24. Report an issue: GitHub.