fullstackhero/dotnet-starter-kit · error · NotFoundException

Parent message not found.

Error message

Parent message not found.

What it means

The parent message lookup by query.ParentMessageId found no row, so the handler throws NotFoundException('Parent message not found.') (HTTP 404). Thrown at the parent query itself when the message id simply doesn't exist (or is invisible due to tenant filtering).

Solutions

  1. Verify the parent message id exists in the current tenant before querying replies
  2. Refresh thread state on the client; handle 404 by removing the stale parent from the UI
  3. Confirm tenant resolution matches the message's tenant
  4. Check DB for soft-deleted message rows if the id should exist
Defensive patterns

Strategy: validation

Validate before calling

if (!parentMessageId) throw new Error('parentMessageId is required');
if (!messages.some(m => m.id === parentMessageId)) throw new Error('parent message not loaded');

Type guard

function hasParent(m: Message | undefined): m is Message { return m !== undefined && !m.deleted; }

Try / catch

try { return await listMessageReplies(parentId); }
catch (e) { if (isNotFound(e)) { removeThreadFromUi(parentId); return []; } throw e; }

Prevention

When it happens

Trigger: Listing replies for a ParentMessageId that does not exist, was hard/soft deleted, or belongs to another tenant.

Common situations: Client holds a reply-to id from a deleted message, cross-tenant id, or stale UI state pointing at a purged thread.

Understand the failure class

Background: "Not found" and "does not exist" errors: why "Task not found", "No such folder", and "Can't find" fire when a lookup comes back empty — this error's family across 14 libraries.

Related errors


AI-assisted analysis of fullstackhero/dotnet-starter-kit@3f2959e683 (2026-09-15). Data as JSON: /api/errors/9587ef9ada292bd4. Report an issue: GitHub.

Appendix: source

Thrown at src/Modules/Chat/Modules.Chat/Features/v1/Messages/ListMessageReplies/ListMessageRepliesQueryHandler.cs:34

    IMediator mediator)
    : IQueryHandler<ListMessageRepliesQuery, ReadOnlyCollection<MessageDto>>
{
    public async ValueTask<ReadOnlyCollection<MessageDto>> Handle(
        ListMessageRepliesQuery query,
        CancellationToken cancellationToken)
    {
        ArgumentNullException.ThrowIfNull(query);
        var userId = currentUser.GetUserId();
        if (userId == Guid.Empty) throw new UnauthorizedException("no current user");
        var currentUserId = userId.ToString();

        // Load the parent so we can authorize the caller through the channel.
        var parent = await db.Messages
            .Where(m => m.Id == query.ParentMessageId)
            .Select(m => new { m.Id, m.ChannelId })
            .FirstOrDefaultAsync(cancellationToken)
            .ConfigureAwait(false)
            ?? throw new NotFoundException("Parent message not found.");

        var channel = await db.Channels.FirstOrDefaultAsync(c => c.Id == parent.ChannelId, cancellationToken)
            .ConfigureAwait(false)
            ?? throw new NotFoundException("Parent message not found.");
        channel.RequireMember(currentUserId);

        IQueryable<Domain.Message> q = db.Messages
            .Where(m => m.ParentMessageId == query.ParentMessageId);

        if (query.Before is { } beforeId)
        {
            q = q.Where(m => m.Id.CompareTo(beforeId) < 0);
        }

        var rows = await q
            .OrderByDescending(m => m.Id)
            .Take(query.PageSize)
            .Include(m => m.Attachments)

View on GitHub (pinned to 3f2959e683)