jackwener/OpenCLI · error · CommandExecutionError

${context} returned malformed message rows.

Error message

${context} returned malformed message rows.

What it means

assertDiscordMessageRowsBelongToTarget iterates every message row and requires each to be a non-null, non-array object. Anything else (null, a string, an array) means the scraper returned structurally broken data, so it throws CommandExecutionError.

Source

Thrown at clis/discord-app/utils.js:600

}

export async function listDiscordThreads(page, limit) {
    return requireRowsWithFields(
        requireArrayEvaluateResult(await page.evaluate(buildListThreadsScript(limit)), 'Discord thread list'),
        ['Thread', 'guild_id', 'channel_id', 'thread_id', 'url'],
        'Discord thread list',
    );
}

export function assertDiscordMessageRowsBelongToTarget(rows, target, context = 'Discord read') {
    if (!target) return rows;
    const expectedChannelId = String(target.thread_id || target.channel_id || '');
    if (!expectedChannelId) {
        throw new CommandExecutionError(`${context} target is missing a stable channel/thread id.`);
    }
    for (const row of rows) {
        if (!row || typeof row !== 'object' || Array.isArray(row)) {
            throw new CommandExecutionError(`${context} returned malformed message rows.`);
        }
        const actualChannelId = row.channel_id == null || row.channel_id === '' ? '' : String(row.channel_id);
        if (!actualChannelId) {
            throw new CommandExecutionError(
                `${context} could not verify returned message target.`,
                `Expected channel/thread id ${expectedChannelId}, but the message row did not include channel_id.`,
            );
        }
        if (actualChannelId && actualChannelId !== expectedChannelId) {
            throw new CommandExecutionError(
                `${context} returned messages from the wrong Discord target.`,
                `Expected channel/thread id ${expectedChannelId}, saw ${actualChannelId}.`,
            );
        }
    }
    return rows;
}

View on GitHub (pinned to 49907e53dc)

Solutions

  1. Filter out non-object rows before calling: rows.filter(r => r && typeof r === 'object' && !Array.isArray(r)).
  2. Update/patch the message scraper so every emitted row is an object with channel_id and id fields.
  3. Check the Discord app DOM state — re-open the channel and retry if the UI was mid-render.
  4. Report/inspect upstream scraper output with "opencli discord-app messages -f json" to see the malformed payload.

Example fix

// before
assertDiscordMessageRowsBelongToTarget(rows, target); // rows contains nulls
// after
const clean = rows.filter((r) => r && typeof r === 'object' && !Array.isArray(r));
assertDiscordMessageRowsBelongToTarget(clean, target);
Defensive patterns

Strategy: type-guard

Validate before calling

const bad = rows.findIndex(r => !r || typeof r !== 'object' || Array.isArray(r));
if (bad !== -1) throw new Error(`Row ${bad} is not a message object`);

Type guard

function isMessageRow(r) { return r !== null && typeof r === 'object' && !Array.isArray(r); }

Try / catch

try { assertDiscordMessageRowsBelongToTarget(rows, target, ctx); } catch (e) { if (String(e.message).includes('malformed message rows')) { rows = rows.filter(isMessageRow); return assertDiscordMessageRowsBelongToTarget(rows, target, ctx); } throw e; }

Prevention

When it happens

Trigger: A row in the rows array passed to assertDiscordMessageRowsBelongToTarget is null, a primitive, or an Array — e.g. the DOM scraper emitted placeholder/null entries when no messages rendered.

Common situations: Reading messages from an empty channel where the scraper returns [null]; a Discord UI update changing selectors so the extractor returns raw strings; bugs in a custom row extractor.

Understand the failure class

Related errors


AI-assisted analysis of jackwener/OpenCLI@49907e53dc (2026-08-29). Data as JSON: /api/errors/7fa45c608449f9a9. Report an issue: GitHub.