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

  1. Reset reply draft state whenever the active channel changes in the UI.
  2. Before sending, assert parent.channelId === channelId client-side.
  3. If calling the API directly, fetch the parent message and confirm its channel matches.
  4. 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

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


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)