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

  1. Send the id of the last message actually present in that channel.
  2. Re-fetch the channel's latest message id if unsure.
  3. Skip/ignore 404 from mark-read — it's safe to clear local unread state anyway.
  4. 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

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)