fullstackhero/dotnet-starter-kit · error · NotFoundException

Message not found.

Error message

Message not found.

What it means

EditMessageCommandHandler throws NotFoundException("Message not found.") when db.Messages has no row matching cmd.MessageId. It is the module's standard 404, and tenant filters mean a message from another tenant is indistinguishable from a missing one. Thrown before any membership or author check.

Solutions

  1. Confirm the message id exists in the caller's tenant before editing.
  2. Handle 404 in the client by refreshing the channel messages and discarding the local optimistic edit.
  3. Check tenant context/headers match the message's tenant.
  4. Guard against concurrent delete/edit races with a reload before submit.

Example fix

// before
// optimistic edit applied against stale message
editLocal(messageId, body);
await api.editMessage(messageId, body); // 404

// after
const fresh = await api.getMessage(messageId).catch(() => null);
if (fresh) { editLocal(messageId, body); await api.editMessage(messageId, body); }
else refreshMessages();
Defensive patterns

Strategy: validation

Validate before calling

const msg = messagesQuery.data?.find(m => m.id === messageId);
if (!msg) { refreshMessages(); return; } // stale id — don't edit

Try / catch

try {
  await api.editMessage(messageId, body);
} catch (e) {
  if (e.status === 404) { rollbackOptimisticEdit(messageId); refreshMessages(); }
  else throw e;
}

Prevention

When it happens

Trigger: Editing a message id that was deleted (possibly by an earlier edit/delete race), never existed, or is outside the caller's tenant scope.

Common situations: Two tabs open on the same message — one already deleted it; optimistic UI applied the edit locally against a stale id; cross-tenant fixture data in integration tests; message removed by retention policy.

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

Appendix: source

Thrown at src/Modules/Chat/Modules.Chat/Features/v1/Messages/EditMessage/EditMessageCommandHandler.cs:28

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

public sealed class EditMessageCommandHandler(
    ChatDbContext db,
    ICurrentUser currentUser,
    IHubContext<AppHub> hub)
    : ICommandHandler<EditMessageCommand, Unit>
{
    public async ValueTask<Unit> Handle(EditMessageCommand 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.");

        // Verify membership through the parent channel (don't leak existence to non-members).
        var channel = await db.Channels.FirstOrDefaultAsync(c => c.Id == message.ChannelId, cancellationToken)
            .ConfigureAwait(false)
            ?? throw new NotFoundException("Message not found.");
        channel.RequireMember(currentUserId);

        message.Edit(cmd.Body, currentUserId); // domain enforces author-only
        await db.SaveChangesAsync(cancellationToken).ConfigureAwait(false);

        await hub.Clients.Group($"channel:{channel.Id}")
            .SendAsync("ChatMessageEdited", message.ToDto(), cancellationToken)
            .ConfigureAwait(false);
        return Unit.Value;
    }
}

View on GitHub (pinned to 3f2959e683)