fullstackhero/dotnet-starter-kit · error · NotFoundException
Channel not found.
Error message
Channel not found.
What it means
The handler queries db.Channels for query.ChannelId and throws NotFoundException (HTTP 404) when no matching row exists. This library throws it so callers get a clean 404 instead of a null-reference later at channel.RequireMember.
Solutions
- Verify the ChannelId exists (and in the current tenant) before calling the endpoint
- Refresh the channel list on the client; clear stale cached channel ids
- Check the X-Tenant / tenant resolution matches the tenant the channel was created in
- If the channel should exist, inspect the Channels table for soft-deletes or migration gaps
Example fix
// before
await apiFetch('/api/v1/channels/'+channelId+'/messages/pinned');
// after
const channel = channels.find(c => c.id === channelId);
if (!channel) throw new Error('channel '+channelId+' not loaded');
await apiFetch('/api/v1/channels/'+channelId+'/messages/pinned'); Defensive patterns
Strategy: validation
Validate before calling
const channel = channels.find(c => c.id === channelId);
if (!channel) throw new Error(`channel ${channelId} not loaded`); Type guard
function channelExists(channels: Channel[], id: string): channels is Channel[] & { length: number } { return channels.some(c => c.id === id); } Try / catch
try { return await getPinnedMessages(channelId); }
catch (e) { if (isNotFound(e)) { invalidateChannelCache(channelId); notifyUser('Channel no longer available'); return []; } throw e; } Prevention
- Refresh channel lists after mutations/deletes
- Never persist channel ids across sessions without revalidation
- Verify tenant header matches the channel's tenant
- Handle 404 by cleaning stale UI references
When it happens
Trigger: GET pinned messages with a ChannelId that does not exist in the Channels table (or exists only in another tenant, hidden by the global tenant query filter).
Common situations: Stale/deleted channel id cached in the client, id from a different tenant, wrong id passed in a route, or tests using seeded ids that were never created.
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/c06c0780f94ae0ac.
Report an issue: GitHub.
Appendix: source
Thrown at src/Modules/Chat/Modules.Chat/Features/v1/Messages/GetPinnedMessages/GetPinnedMessagesQueryHandler.cs:30
public sealed class GetPinnedMessagesQueryHandler(
ChatDbContext db,
ICurrentUser currentUser,
IMediator mediator)
: IQueryHandler<GetPinnedMessagesQuery, ReadOnlyCollection<MessageDto>>
{
public async ValueTask<ReadOnlyCollection<MessageDto>> Handle(
GetPinnedMessagesQuery 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);
var rows = await db.Messages
.Where(m => m.ChannelId == query.ChannelId && m.IsPinned)
.OrderByDescending(m => m.PinnedAtUtc)
.Include(m => m.Attachments)
.Include(m => m.Mentions)
.AsNoTracking()
.ToListAsync(cancellationToken)
.ConfigureAwait(false);
var dtos = rows.Select(m => m.ToDto()).ToList();
var resolved = await ChatAttachmentUrls.ResolveAsync(dtos, mediator, cancellationToken).ConfigureAwait(false);
return resolved.AsReadOnly();
}
}
View on GitHub (pinned to 3f2959e683)