jackwener/OpenCLI · error · CommandExecutionError

Xiaohongshu media extraction returned malformed payload.

Error message

Xiaohongshu media extraction returned malformed payload.

What it means

CommandExecutionError thrown when the page.evaluate extraction (buildDownloadExtractJs) returns something that is not an object with a `media` array. This means the extraction script did not run in the expected page context or the page DOM changed so the script returned null/undefined/a primitive. It guards downstream code that assumes `data.media` is iterable.

Source

Thrown at clis/xiaohongshu/download.js:231

    args: [
        { name: 'note-id', positional: true, required: true, help: 'Full Xiaohongshu note URL with xsec_token, or xhslink short link' },
        { name: 'output', default: './xiaohongshu-downloads', help: 'Output directory' },
    ],
    columns: ['index', 'type', 'status', 'size'],
    func: async (page, kwargs) => {
        const rawInput = String(kwargs['note-id']);
        const output = kwargs.output;
        const noteId = parseNoteId(rawInput);
        await page.goto(buildNoteUrl(rawInput, { allowShortLink: true, commandName: 'xiaohongshu download' }));
        await page.wait({ time: 1 + Math.random() * 2 });
        const data = await page.evaluate(buildDownloadExtractJs(noteId));
        if (data?.securityBlock) {
            throw new CliError('SECURITY_BLOCK', 'Xiaohongshu security block: the note detail page was blocked by risk control.', /^https?:\/\//.test(rawInput)
                ? 'The page may be temporarily restricted. Try again later or from a different session.'
                : 'Try using a full URL from search results (with xsec_token) instead of a bare note ID.');
        }
        if (!data || typeof data !== 'object' || !Array.isArray(data.media)) {
            throw new CommandExecutionError('Xiaohongshu media extraction returned malformed payload.');
        }
        if (data.media.length === 0) {
            throw new EmptyResultError('xiaohongshu download', 'No downloadable media found on this note.');
        }
        // Extract cookies for authenticated downloads
        const cookies = formatCookieHeader(await page.getCookies({ domain: 'xiaohongshu.com' }));
        const resolvedNoteId = typeof data.noteId === 'string' && data.noteId.trim()
            ? data.noteId.trim()
            : noteId;
        return downloadMedia(data.media, {
            output,
            subdir: resolvedNoteId,
            cookies,
            filenamePrefix: resolvedNoteId,
            timeout: 60000,
        });
    },
});

View on GitHub (pinned to 49907e53dc)

Solutions

  1. Re-run once — it may be a transient render race; increase the pre-evaluate wait.
  2. Confirm you can see the note normally in a browser session with the same cookies.
  3. Inspect the raw evaluate return to see what the page actually returned and update buildDownloadExtractJs selectors for the new DOM.
  4. Ensure the input URL actually resolves to a note detail page (not a profile or redirect).

Example fix

// before
await page.goto(url);
const data = await page.evaluate(buildDownloadExtractJs(noteId));
// after
await page.goto(url);
await page.wait({ time: 2 + Math.random() * 2 }); // let note content mount
const data = await page.evaluate(buildDownloadExtractJs(noteId));
if (!data || !Array.isArray(data?.media)) throw new Error('unexpected page: ' + await page.title());
Defensive patterns

Strategy: type-guard

Validate before calling

null

Type guard

function isExtractPayload(data) {
  return data !== null && typeof data === 'object' && Array.isArray(data.media);
}

Try / catch

try {
  await cli.run('xiaohongshu download', { input: url });
} catch (err) {
  if (/malformed payload/.test(err.message)) {
    // page didn't render as expected: increase wait, retry once, then surface
    await sleep(3000);
    return downloadNote(url);
  }
  throw err;
}

Prevention

When it happens

Trigger: page.evaluate returns null or a non-object because the note page did not render the expected structure; the extraction script threw silently and returned undefined; a DOM redesign changed the fields buildDownloadExtractJs reads; the script ran on a login/redirect page instead of the note page.

Common situations: Xiaohongshu shipped a front-end update that broke the extractor; the page redirected to login or a captcha (without setting securityBlock); a race where evaluate ran before the note content mounted.

Understand the failure class

Related errors


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