fullstackhero/dotnet-starter-kit · warning · NotFoundException
Message not found in this channel.
Error message
Message not found in this channel.
What it means
MarkChannelReadCommandHandler verifies the marker message exists in the target channel via db.Messages.AnyAsync(m => m.Id == cmd.MessageId && m.ChannelId == cmd.ChannelId); if not, it throws NotFoundException. The message id must reference a message that both exists AND belongs to the same channel.
Solutions
- Send the id of the last message actually present in that channel.
- Re-fetch the channel's latest message id if unsure.
- Skip/ignore 404 from mark-read — it's safe to clear local unread state anyway.
- Add a channel-deleted/message-deleted handler that resets local unread pointers.
Example fix
// before markRead(channel.id, lastVisibleMessage.channelId); // wrong field // after markRead(channel.id, lastVisibleMessage.id); // must be the message's own id in this channel
Defensive patterns
Strategy: validation
Validate before calling
if (!messageId || !lastMessageInChannel(channelId, messageId)) {
messageId = await fetchLatestMessageId(channelId);
} Type guard
const isValidMarker = (m: {id: string; channelId: string} | null, channelId: string): m is {id: string; channelId: string} =>
!!m && m.channelId === channelId && typeof m.id === 'string'; Try / catch
try { ... } catch (e) {
if (isNotFound(e)) { clearLocalUnread(channelId); } // message gone; safe to clear
else throw e;
} Prevention
- Send the message's own id, not the channel id — watch for swapped fields.
- After message deletion, refresh the latest-message pointer before marking read.
- Treat mark-read 404s as benign and clear local unread state.
- Keep unread pointers in sync via real-time channel events.
When it happens
Trigger: Marking read with a messageId from a different channel; the message was hard/soft deleted; a client passed the channel id instead of the message id; race where the message was deleted between display and mark-read.
Common situations: Mixed-up ids in UI code (lastMessageId vs channelId swap); stale unread pointers after message deletion; optimistic UI clearing a badge for a message that no longer exists; replayed queued read events.
Understand the failure class
Background: 'Could not be found', 'does not exist', 'not found in database': the resource-not-found family when an ID, slug, key, or URI lookup comes back empty — this error's family across 20 libraries.
Related errors
AI-assisted analysis of fullstackhero/dotnet-starter-kit@3f2959e683 (2026-09-15).
Data as JSON: /api/errors/7ffa2656c03b7241.
Report an issue: GitHub.
Appendix: source
Thrown at src/Modules/Chat/Modules.Chat/Features/v1/Channels/MarkChannelRead/MarkChannelReadCommandHandler.cs:35
: ICommandHandler<MarkChannelReadCommand, Unit>
{
public async ValueTask<Unit> Handle(MarkChannelReadCommand 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);
// Verify the marker message actually exists in this channel.
var exists = await db.Messages
.AnyAsync(m => m.Id == cmd.MessageId && m.ChannelId == cmd.ChannelId, cancellationToken)
.ConfigureAwait(false);
if (!exists) throw new NotFoundException("Message not found in this channel.");
channel.MarkRead(currentUserId, cmd.MessageId);
await db.SaveChangesAsync(cancellationToken).ConfigureAwait(false);
// Push to the user's other tabs so the badge clears everywhere at once.
await hub.Clients.Group($"user:{currentUserId}")
.SendAsync("ChatChannelRead",
new { channelId = cmd.ChannelId, lastReadMessageId = cmd.MessageId },
cancellationToken)
.ConfigureAwait(false);
// Push to the channel group so members update read receipts in real time. The reader's own
// connections also receive this (alongside the user-scoped push); the client handler is idempotent.
await hub.Clients.Group($"channel:{cmd.ChannelId}")
.SendAsync("ChatChannelMemberRead",
new { channelId = cmd.ChannelId, userId = currentUserId, lastReadMessageId = cmd.MessageId },
cancellationToken)
.ConfigureAwait(false);View on GitHub (pinned to 3f2959e683)