fullstackhero/dotnet-starter-kit · error · CustomException

Cannot post a comment without an authenticated author.

Error message

Cannot post a comment without an authenticated author.

What it means

AddTicketCommentCommandHandler resolves the current user via currentUser.GetUserId(); if it returns Guid.Empty (no authenticated principal), the handler throws a 401 CustomException because a comment cannot be attributed to an author. The guard protects comment auditability.

Solutions

  1. Ensure the request carries a valid Bearer token before calling the add-comment endpoint.
  2. Refresh an expired JWT and retry the request.
  3. Verify the endpoint requires authorization ([Authorize] / permission gate) so anonymous calls are rejected earlier.
  4. In tests, configure ICurrentUser.GetUserId() to return a non-empty Guid.

Example fix

// before
await http.PostAsync($"/tickets/v1/{id}/comments", content); // no auth header
// after
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token);
await http.SendAsync(request);
Defensive patterns

Strategy: validation

Validate before calling

if (currentUser.GetUserId() == Guid.Empty)
    throw new UnauthorizedAccessException("Sign in before posting a comment.");

Type guard

static bool IsAuthenticated(ICurrentUser user) => user.GetUserId() != Guid.Empty;

Try / catch

try { await mediator.Send(new AddTicketCommentCommand { TicketId = id, Body = body }); }
catch (CustomException ex) when (ex.StatusCode == HttpStatusCode.Unauthorized) { await refreshTokenAndRetry(); }

Prevention

When it happens

Trigger: POSTing a ticket comment with no/invalid JWT, an expired token stripped by auth middleware, or an anonymous endpoint invocation where ICurrentUser.GetUserId() yields Guid.Empty.

Common situations: Calling the API without the Authorization header; token expired client-side but request still routed; integration tests invoking the handler without a mocked ICurrentUser returning a real user id.

Understand the failure class

Related errors


AI-assisted analysis of fullstackhero/dotnet-starter-kit@3f2959e683 (2026-09-15). Data as JSON: /api/errors/6a85ae45bdacf0b9. Report an issue: GitHub.

Appendix: source

Thrown at src/Modules/Tickets/Modules.Tickets/Features/v1/Tickets/AddTicketComment/AddTicketCommentCommandHandler.cs:23

using FSH.Modules.Tickets.Data;
using Mediator;
using Microsoft.EntityFrameworkCore;

namespace FSH.Modules.Tickets.Features.v1.Tickets.AddTicketComment;

public sealed class AddTicketCommentCommandHandler(
    TicketsDbContext dbContext,
    ICurrentUser currentUser)
    : ICommandHandler<AddTicketCommentCommand, Guid>
{
    public async ValueTask<Guid> Handle(AddTicketCommentCommand command, CancellationToken cancellationToken)
    {
        ArgumentNullException.ThrowIfNull(command);

        var authorId = currentUser.GetUserId();
        if (authorId == Guid.Empty)
        {
            throw new CustomException(
                "Cannot post a comment without an authenticated author.",
                (IEnumerable<string>?)null,
                HttpStatusCode.Unauthorized);
        }

        // Load the Comments collection up front so EF's change tracker detects the new TicketComment
        // (added via the aggregate) as an INSERT rather than missing it during change detection.
        var ticket = await dbContext.Tickets
            .Include(t => t.Comments)
            .FirstOrDefaultAsync(t => t.Id == command.TicketId, cancellationToken)
            .ConfigureAwait(false)
            ?? throw new NotFoundException($"Ticket {command.TicketId} not found.");

        var commentId = ticket.AddComment(authorId, command.Body);
        await dbContext.SaveChangesAsync(cancellationToken).ConfigureAwait(false);
        return commentId;
    }
}

View on GitHub (pinned to 3f2959e683)