fullstackhero/dotnet-starter-kit · error · NotFoundException
Channel not found.
Error message
Channel not found.
What it means
The handler looks up db.Channels by query.ChannelId and throws NotFoundException('Channel not found.') (HTTP 404) when no row matches. Thrown before the membership check so unauthorized callers cannot probe channel membership.
Solutions
- Confirm the channel id exists in the current tenant before listing messages
- Refresh the channel list in the client to drop stale ids
- Verify tenant resolution (header/subdomain) matches the channel's tenant
- Check the DB for the channel row / soft-delete state
Defensive patterns
Strategy: validation
Validate before calling
const channel = channels.find(c => c.id === channelId);
if (!channel) throw new Error(`unknown channel ${channelId}`); Type guard
function hasChannel(c: Channel | undefined): c is Channel { return c !== undefined; } Try / catch
try { return await listChannelMessages(channelId); }
catch (e) { if (isNotFound(e)) { dropChannelFromSidebar(channelId); return []; } throw e; } Prevention
- Revalidate channel ids after deletes
- Match tenant context with channel tenant
- Clean stale ids from routing state
- Return empty + message on 404 rather than crashing
When it happens
Trigger: Listing messages for a ChannelId that does not exist, was deleted, or belongs to another tenant (filtered out by the tenant query filter).
Common situations: Client kept a channel id after the channel was deleted, cross-tenant id reuse, or a typo'd/wrong route parameter.
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/934391d95c12e2e4.
Report an issue: GitHub.
Appendix: source
Thrown at src/Modules/Chat/Modules.Chat/Features/v1/Messages/ListChannelMessages/ListChannelMessagesQueryHandler.cs:30
public sealed class ListChannelMessagesQueryHandler(
ChatDbContext db,
ICurrentUser currentUser,
IMediator mediator)
: IQueryHandler<ListChannelMessagesQuery, ReadOnlyCollection<MessageDto>>
{
public async ValueTask<ReadOnlyCollection<MessageDto>> Handle(
ListChannelMessagesQuery query,
CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(query);
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 == query.ChannelId, cancellationToken)
.ConfigureAwait(false)
?? throw new NotFoundException("Channel not found.");
channel.RequireMember(currentUserId);
// Top-level only (no thread replies). Guid v7 monotonic → Id desc = time desc.
IQueryable<Domain.Message> q = db.Messages
.Where(m => m.ChannelId == query.ChannelId && m.ParentMessageId == null);
if (query.Before is { } beforeId)
{
q = q.Where(m => m.Id.CompareTo(beforeId) < 0);
}
var rows = await q
.OrderByDescending(m => m.Id)
.Take(query.PageSize)
.Include(m => m.Attachments)
.AsNoTracking()
.ToListAsync(cancellationToken)
.ConfigureAwait(false);View on GitHub (pinned to 3f2959e683)