fullstackhero/dotnet-starter-kit · error · CustomException
A closed ticket cannot be resolved — reopen it first.
Error message
A closed ticket cannot be resolved — reopen it first.
What it means
Ticket.Resolve enforces the domain rule that a Closed ticket cannot be resolved; the ticket must be reopened first. It throws CustomException with HTTP 409 Conflict. This is a guard against illegal status transitions on the Ticket aggregate.
Solutions
- Reopen the ticket first (Reopen method/endpoint), then call Resolve.
- Reload the ticket to get fresh status before resolving — the UI state may be stale.
- Handle the 409 in the client and prompt the user to reopen the ticket.
- Check Status via the ticket query endpoint before issuing Resolve.
Example fix
// before
ticket.Resolve(resolutionNote); // 409 if closed
// after
if (ticket.Status == TicketStatus.Closed)
{
ticket.Reopen();
}
ticket.Resolve(resolutionNote); Defensive patterns
Strategy: validation
Validate before calling
if (ticket.status === TicketStatus.Closed) {
throw new Error('Reopen the ticket before resolving.');
}
ticket.resolve(resolutionNote); Type guard
bool CanResolve(Ticket t) => t.Status != TicketStatus.Closed;
Try / catch
try
{
ticket.Resolve(resolutionNote);
}
catch (CustomException ex) when (ex.StatusCode == HttpStatusCode.Conflict)
{
// ticket was closed concurrently — reload and prompt to reopen
await repo.ReloadAsync(ticket.Id, ct);
} Prevention
- Check ticket status immediately before resolving; reload from the server if the view is stale.
- Disable Resolve controls in the UI for closed tickets.
- Handle 409 responses by refreshing the ticket and informing the user.
- Use optimistic-concurrency-aware updates to detect concurrent close/resolve races.
When it happens
Trigger: Calling Resolve (or the resolve endpoint) on a ticket whose Status is TicketStatus.Closed.
Common situations: Two agents acting concurrently — one closed the ticket while another was writing a resolution; client UI state stale (showed an open ticket that was actually closed); automated scripts replaying old actions against closed tickets.
Understand the failure class
Background: "Invalid state transition" errors: "status must be X, actually Y", "already rejected/charging/uninstalled", "cannot ... while running" — what they mean when a library rejects your call — this error's family across 31 libraries.
Related errors
- Only a resolved ticket can be closed — current status is
- A closed ticket cannot accept new comments — reopen it…
- Cannot a ticket in status — reopen it first.
- A closed ticket cannot be edited — reopen it first.
- Top-up request cannot be rejected because it is (only…
AI-assisted analysis of fullstackhero/dotnet-starter-kit@3f2959e683 (2026-09-15).
Data as JSON: /api/errors/e3844263f67aaa39.
Report an issue: GitHub.
Appendix: source
Thrown at src/Modules/Tickets/Modules.Tickets/Domain/Ticket.cs:120
// ticket sends it back to Open since no owner is pushing it forward.
if (assigneeUserId is not null && Status == TicketStatus.Open)
{
TransitionStatus(TicketStatus.InProgress);
}
else if (assigneeUserId is null && Status == TicketStatus.InProgress)
{
TransitionStatus(TicketStatus.Open);
}
AddDomainEvent(DomainEvent.Create<TicketAssignedDomainEvent>(
(id, ts) => new TicketAssignedDomainEvent(Id, previous, assigneeUserId, id, ts)));
}
public void Resolve(string? resolutionNote)
{
if (Status == TicketStatus.Closed)
{
throw new CustomException(
"A closed ticket cannot be resolved — reopen it first.",
(IEnumerable<string>?)null,
HttpStatusCode.Conflict);
}
if (Status == TicketStatus.Resolved)
{
return;
}
ResolutionNote = string.IsNullOrWhiteSpace(resolutionNote) ? null : resolutionNote.Trim();
ResolvedAtUtc = DateTime.UtcNow;
UpdatedAtUtc = DateTime.UtcNow;
TransitionStatus(TicketStatus.Resolved);
}
/// <summary>
/// Finalizes a resolved ticket (Resolved → Closed). Idempotent when already Closed;
/// rejects any other source state with a 409 so the documented state machine holds.View on GitHub (pinned to 3f2959e683)