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
- Confirm the message id exists in the caller's tenant before editing.
- Handle 404 in the client by refreshing the channel messages and discarding the local optimistic edit.
- Check tenant context/headers match the message's tenant.
- 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
- Reload the message before submitting edits in long-lived views.
- Roll back optimistic updates on 404.
- Invalidate message caches on delete events from other sessions.
- Keep test tenants consistent between message and channel seeds.
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)