fullstackhero/dotnet-starter-kit · warning · CustomException
Cannot reply to a reply — threads are single-level only.
Error message
Cannot reply to a reply — threads are single-level only.
What it means
Chat threads are one level deep: the handler rejects replying to a message that is itself a reply (parent.ParentMessageId.HasValue) with a 400 CustomException. Only top-level messages can be replied to.
Solutions
- Only enable reply on messages where parentMessageId is null.
- When a reply to a reply is attempted, retarget the request to the thread's root message id instead.
- Render child replies with a 'View thread' action pointing to the root, not a reply action.
- Validate parentId depth client-side before submitting.
Example fix
// before const target = msg.parentMessageId ?? msg.id; // after // retarget reply to the thread root (already the fix) or: if (msg.parentMessageId) disableReplyButton(msg);
Defensive patterns
Strategy: validation
Validate before calling
if (parent?.parentMessageId) throw new Error('Threads are single-level; reply to the root message instead'); Type guard
const isThreadRoot = (m) => m.parentMessageId == null; // only show Reply when isThreadRoot(m)
Try / catch
try { await sendReply(cmd); }
catch (e) { if (e.status === 400 && /single-level/.test(e.message)) { retargetToRootAndRetry(); } else throw e; } Prevention
- Gate Reply UI on parentMessageId == null
- Retarget replies to the thread root automatically
- Document the one-level threading rule for API consumers
When it happens
Trigger: Passing a ParentMessageId that points to an already-nested reply — e.g. a UI that renders a 'reply' button on every message in a thread including child replies, or an API client walking a thread recursively.
Common situations: Threaded view rendering reply buttons on child messages, automation or tests using a child message id as parent, or a client model that doesn't distinguish parent vs reply messages.
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
- Parent message belongs to a different channel.
- Cannot DM yourself.
- Parent message not found.
- A category cannot be its own parent.
- Setting this parent would create a cycle.
AI-assisted analysis of fullstackhero/dotnet-starter-kit@3f2959e683 (2026-09-15).
Data as JSON: /api/errors/0260345f0eccdcb0.
Report an issue: GitHub.
Appendix: source
Thrown at src/Modules/Chat/Modules.Chat/Features/v1/Messages/SendMessage/SendMessageCommandHandler.cs:54
.ConfigureAwait(false)
?? throw new NotFoundException("Channel not found.");
channel.RequireMember(currentUserId);
Message? parent = null;
if (cmd.ParentMessageId is { } parentId)
{
parent = await db.Messages.FirstOrDefaultAsync(m => m.Id == parentId, cancellationToken)
.ConfigureAwait(false)
?? throw new NotFoundException("Parent message not found.");
if (parent.ChannelId != channel.Id)
{
throw new CustomException("Parent message belongs to a different channel.", (IEnumerable<string>?)null, HttpStatusCode.BadRequest);
}
if (parent.ParentMessageId.HasValue)
{
// 1-level deep only per spec.
throw new CustomException("Cannot reply to a reply — threads are single-level only.", (IEnumerable<string>?)null, HttpStatusCode.BadRequest);
}
}
// Parse @username tokens to user ids; self-mentions and unresolved tokens are dropped silently.
// Only resolved *other* users attach as MessageMention rows + fire an event. Body may be empty.
var rawMatches = MentionParser.Parse(cmd.Body ?? string.Empty);
var distinctNames = rawMatches.Select(m => m.Username)
.Distinct(StringComparer.OrdinalIgnoreCase)
.ToArray();
var resolved = distinctNames.Length == 0
? new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
: (Dictionary<string, string>)await mentionResolver
.ResolveUserIdsAsync(distinctNames, cancellationToken)
.ConfigureAwait(false);
var parsedMentions = new List<Message.ParsedMention>();
var notifyUserIds = new HashSet<string>(StringComparer.Ordinal);
foreach (var match in rawMatches)View on GitHub (pinned to 3f2959e683)