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

  1. Confirm the channel id exists in the current tenant before listing messages
  2. Refresh the channel list in the client to drop stale ids
  3. Verify tenant resolution (header/subdomain) matches the channel's tenant
  4. 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

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)