fullstackhero/dotnet-starter-kit · error · NotFoundException

Parent message not found.

Error message

Parent message not found.

What it means

When SendMessageCommand carries a ParentMessageId, the handler loads that message and throws NotFoundException("Parent message not found.") if it does not exist. Threads are modeled by pointing a message at its parent, so a dangling parent id is rejected up front.

Solutions

  1. Re-fetch the thread before submitting the reply and handle 404 by disabling the composer.
  2. Refresh the message list after deletions (listen to the SignalR message-deleted event).
  3. Verify the parent id belongs to the same channel/tenant as the target channel.
  4. Check for typos or stale ids in the client state.

Example fix

// before
const parent = messages.find(m => m.id === parentId); // may be deleted
await sendMessage({ channelId, parentId, body });
// after
if (!messages.some(m => m.id === parentId)) return; // parent gone, drop reply
await sendMessage({ channelId, parentId, body });
Defensive patterns

Strategy: validation

Validate before calling

if (parentId && !messages.some(m => m.id === parentId)) throw new Error('Parent message no longer exists');

Try / catch

try { await sendReply(cmd); }
catch (e) { if (e.status === 404) { closeReplyComposer(); refetchThread(); } else throw e; }

Prevention

When it happens

Trigger: Replying to a message id that was deleted, never existed, or belongs to another tenant (hidden by the global query filter).

Common situations: Client kept a reply composer open after the parent message was deleted by a moderator, race between delete and reply submit, or copying a message id from another tenant/environment.

Understand the failure class

Background: Record Not Found Errors: "not found", RecordNotFound, and "was not found" — what they mean and how to fix them — this error's family across 28 libraries.

Related errors


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

Appendix: source

Thrown at src/Modules/Chat/Modules.Chat/Features/v1/Messages/SendMessage/SendMessageCommandHandler.cs:46

    public async ValueTask<MessageDto> Handle(SendMessageCommand cmd, CancellationToken cancellationToken)
    {
        ArgumentNullException.ThrowIfNull(cmd);
        var userId = currentUser.GetUserId();
        if (userId == Guid.Empty) throw new UnauthorizedException("no current user");
        var currentUserId = userId.ToString();

        var channel = await db.Channels.FirstOrDefaultAsync(c => c.Id == cmd.ChannelId, cancellationToken)
            .ConfigureAwait(false)
            ?? throw new NotFoundException("Channel not found.");

        channel.RequireMember(currentUserId);

        Message? parent = null;
        if (cmd.ParentMessageId is { } parentId)
        {
            parent = await db.Messages.FirstOrDefaultAsync(m => m.Id == parentId, cancellationToken)
                .ConfigureAwait(false)
                ?? throw new NotFoundException("Parent message not found.");
            if (parent.ChannelId != channel.Id)
            {
                throw new CustomException("Parent message belongs to a different channel.", (IEnumerable<string>?)null, HttpStatusCode.BadRequest);
            }
            if (parent.ParentMessageId.HasValue)
            {
                // 1-level deep only per spec.
                throw new CustomException("Cannot reply to a reply — threads are single-level only.", (IEnumerable<string>?)null, HttpStatusCode.BadRequest);
            }
        }

        // Parse @username tokens to user ids; self-mentions and unresolved tokens are dropped silently.
        // Only resolved *other* users attach as MessageMention rows + fire an event. Body may be empty.
        var rawMatches = MentionParser.Parse(cmd.Body ?? string.Empty);
        var distinctNames = rawMatches.Select(m => m.Username)
            .Distinct(StringComparer.OrdinalIgnoreCase)
            .ToArray();
        var resolved = distinctNames.Length == 0

View on GitHub (pinned to 3f2959e683)