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
- Re-fetch the thread before submitting the reply and handle 404 by disabling the composer.
- Refresh the message list after deletions (listen to the SignalR message-deleted event).
- Verify the parent id belongs to the same channel/tenant as the target channel.
- 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
- Refresh threads on message-deleted events
- Close reply composers whose parent disappears
- Don't reuse ids copied from other environments
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
- User is not a member.
- Channel not found.
- Channel not found.
- Message not found in this channel.
- Channel not found.
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 == 0View on GitHub (pinned to 3f2959e683)