{"record":{"id":"900e899e077f6f1f","repo":"fullstackhero/dotnet-starter-kit","slug":"no-current-user-markchannelreadcommandhandler","errorCode":null,"errorMessage":"no current user","messagePattern":"no current user","errorType":"exception","errorClass":"UnauthorizedException","httpStatus":401,"severity":"error","filePath":"src/Modules/Chat/Modules.Chat/Features/v1/Channels/MarkChannelRead/MarkChannelReadCommandHandler.cs","lineNumber":23,"sourceCode":"using FSH.Modules.Chat.Data;\nusing FSH.Modules.Chat.Features.v1.Internal;\nusing Mediator;\nusing Microsoft.AspNetCore.SignalR;\nusing Microsoft.EntityFrameworkCore;\n\nnamespace FSH.Modules.Chat.Features.v1.Channels.MarkChannelRead;\n\npublic sealed class MarkChannelReadCommandHandler(\n    ChatDbContext db,\n    ICurrentUser currentUser,\n    IHubContext<AppHub> hub)\n    : ICommandHandler<MarkChannelReadCommand, Unit>\n{\n    public async ValueTask<Unit> Handle(MarkChannelReadCommand cmd, CancellationToken cancellationToken)\n    {\n        ArgumentNullException.ThrowIfNull(cmd);\n        var userId = currentUser.GetUserId();\n        if (userId == Guid.Empty) throw new UnauthorizedException(\"no current user\");\n        var currentUserId = userId.ToString();\n\n        var channel = await db.Channels.FirstOrDefaultAsync(c => c.Id == cmd.ChannelId, cancellationToken)\n            .ConfigureAwait(false)\n            ?? throw new NotFoundException(\"Channel not found.\");\n        channel.RequireMember(currentUserId);\n\n        // Verify the marker message actually exists in this channel.\n        var exists = await db.Messages\n            .AnyAsync(m => m.Id == cmd.MessageId && m.ChannelId == cmd.ChannelId, cancellationToken)\n            .ConfigureAwait(false);\n        if (!exists) throw new NotFoundException(\"Message not found in this channel.\");\n\n        channel.MarkRead(currentUserId, cmd.MessageId);\n        await db.SaveChangesAsync(cancellationToken).ConfigureAwait(false);\n\n        // Push to the user's other tabs so the badge clears everywhere at once.\n        await hub.Clients.Group($\"user:{currentUserId}\")","sourceCodeStart":5,"sourceCodeEnd":41,"githubUrl":"https://github.com/fullstackhero/dotnet-starter-kit/blob/3f2959e683e9f83f13e55e1678c9119f63c7e8e5/src/Modules/Chat/Modules.Chat/Features/v1/Channels/MarkChannelRead/MarkChannelReadCommandHandler.cs#L5-L41","documentation":"MarkChannelReadCommandHandler requires a resolved current user; Guid.Empty from currentUser.GetUserId() triggers UnauthorizedException before any channel lookup. Read-state updates are per-user, so an identity is mandatory.","triggerScenarios":"Calling the mark-channel-read command (POST /channels/{channelId}/read) with no/invalid/expired JWT, so the handler can't attribute the read marker to a user.","commonSituations":"SignalR-style badge clearing fired from a background tab whose token expired; auth header omitted from a fire-and-forget fetch; identity not yet hydrated on app resume.","solutions":["Ensure a valid access token is attached to every read-marker request.","Refresh expired tokens proactively before emitting read receipts.","Wire a global 401 interceptor that re-authenticates and replays the request.","Check JWT claim configuration if tokens are valid but claims are unmapped."],"exampleFix":"// before\napi.post(`/channels/${id}/read`, { messageId }).catch(() => {});\n// after\nawait withAuth(() => api.post(`/channels/${id}/read`, { messageId }));","handlingStrategy":"try-catch","validationCode":"if (!accessToken || isTokenExpired(accessToken)) await refreshToken();","typeGuard":null,"tryCatchPattern":"try { ... } catch (e) {\n  if (isUnauthorized(e)) { await reauthenticate(); replayMarkRead(); }\n  else if (isNotFound(e)) { /* drop marker */ }\n  else throw e;\n}","preventionTips":["Never fire read receipts from a bare fetch without the auth header.","Refresh tokens on window focus for background-tab badge updates.","Queue read markers and replay after re-auth instead of dropping them.","Centralize 401 handling in one interceptor."],"tags":["auth","jwt","read-receipt"],"backgroundTag":"authentication-required","analyzedSha":"3f2959e683e9f83f13e55e1678c9119f63c7e8e5","analyzedAt":"2026-09-15T22:20:53.684Z","contentChangedAt":"2026-09-15T22:20:53.684Z","schemaVersion":2},"datasetVersion":"2026-09-16T04:17:20.429Z"}