fullstackhero/dotnet-starter-kit · error · NotFoundException

Message not found.

Error message

Message not found.

What it means

DeleteMessageCommandHandler throws NotFoundException("Message not found.") when the message row itself is missing (FirstOrDefaultAsync on db.Messages by cmd.MessageId returns null). Standard 404 for a nonexistent aggregate. The tenant query filter can also hide an existing message belonging to another tenant.

Solutions

  1. Check the message exists in the current tenant before deleting; treat 404 on a second delete as success (idempotency).
  2. Refresh the message list on the client after any delete so stale ids are not reused.
  3. Verify the tenant context of the request matches the message's tenant.
  4. Confirm migrations/seed data if the message should exist.

Example fix

// before
await deleteMessage(messageId); // second click throws 404

// after
try { await deleteMessage(messageId); }
catch (NotFoundException) { /* already deleted — treat as success */ }
Defensive patterns

Strategy: validation

Validate before calling

const msg = messagesQuery.data?.find(m => m.id === messageId);
if (!msg) return; // already gone — skip delete

Try / catch

try {
  await api.deleteMessage(messageId);
} catch (e) {
  if (e.status === 404) { removeLocal(messageId); /* idempotent success */ }
  else throw e;
}

Prevention

When it happens

Trigger: DELETE /messages/{id} with an id that was already deleted, never existed, or is not visible in the current tenant's query scope.

Common situations: Double-clicking delete so the second call targets an already-deleted message; stale client cache holding removed messages; cross-tenant id collision in tests; message hard-deleted by a moderator while the user was viewing it.

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/acdce83a03ed66ea. Report an issue: GitHub.

Appendix: source

Thrown at src/Modules/Chat/Modules.Chat/Features/v1/Messages/DeleteMessage/DeleteMessageCommandHandler.cs:31

namespace FSH.Modules.Chat.Features.v1.Messages.DeleteMessage;

public sealed class DeleteMessageCommandHandler(
    ChatDbContext db,
    ICurrentUser currentUser,
    IUserPermissionService permissions,
    IHubContext<AppHub> hub)
    : ICommandHandler<DeleteMessageCommand, Unit>
{
    public async ValueTask<Unit> Handle(DeleteMessageCommand 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 message = await db.Messages.FirstOrDefaultAsync(m => m.Id == cmd.MessageId, cancellationToken)
            .ConfigureAwait(false)
            ?? throw new NotFoundException("Message not found.");

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

        bool isModerator = await permissions
            .HasPermissionAsync(currentUserId, ChatPermissions.Messages.DeleteAny, cancellationToken)
            .ConfigureAwait(false);

        message.SoftDelete(currentUserId, isModerator);

        // If this was a thread reply, decrement the parent's ReplyCount.
        if (message.ParentMessageId is { } parentId)
        {
            var parent = await db.Messages.FirstOrDefaultAsync(m => m.Id == parentId, cancellationToken)
                .ConfigureAwait(false);
            parent?.DecrementReplyCount();

View on GitHub (pinned to 3f2959e683)