fullstackhero/dotnet-starter-kit · warning · CustomException
Parent message belongs to a different channel.
Error message
Parent message belongs to a different channel.
What it means
A reply's ParentMessageId must reference a message in the same channel. The handler compares parent.ChannelId to the target channel and throws a 400 CustomException("Parent message belongs to a different channel.") on mismatch, preventing cross-channel threads.
Solutions
- Reset reply draft state whenever the active channel changes in the UI.
- Before sending, assert parent.channelId === channelId client-side.
- If calling the API directly, fetch the parent message and confirm its channel matches.
- Return the user to the parent's channel if cross-channel reply was intended.
Example fix
// before
sendMessage({ channelId: activeChannelId, parentId: draft.parentId, body });
// after
if (draft.channelId !== activeChannelId) clearReplyDraft();
else sendMessage({ channelId: activeChannelId, parentId: draft.parentId, body }); Defensive patterns
Strategy: validation
Validate before calling
const parent = messages.find(m => m.id === parentId);
if (parent && parent.channelId !== channelId) throw new Error('Reply must target a message in the same channel'); Try / catch
try { await sendReply(cmd); }
catch (e) { if (e.status === 400 && /different channel/.test(e.message)) { clearReplyDraft(); notify('Reply draft reset'); } else throw e; } Prevention
- Clear reply drafts on channel switch
- Bind the draft to its channel id and compare before send
When it happens
Trigger: Submitting a reply where ParentMessageId points at a message from another channel than cmd.ChannelId — typically from buggy client state or an API consumer hand-assembling the request.
Common situations: UI switched channels but kept the reply draft and its parentId, deep-link/share copied a reply across channels, or automated scripts reusing ids.
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
- Cannot reply to a reply — threads are single-level only.
- 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/02840a68f9e302ff.
Report an issue: GitHub.
Appendix: source
Thrown at src/Modules/Chat/Modules.Chat/Features/v1/Messages/SendMessage/SendMessageCommandHandler.cs:49
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 == cmd.ChannelId, cancellationToken)
.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)View on GitHub (pinned to 3f2959e683)