{"record":{"id":"9298fa54b3782165","repo":"jackwener/OpenCLI","slug":"unexpected-rednote-search-extraction-payload-shape","errorCode":null,"errorMessage":"Unexpected Rednote search extraction payload shape; expected an array of rows.","messagePattern":"Unexpected Rednote search extraction payload shape; expected an array of rows\\.","errorType":"exception","errorClass":"CommandExecutionError","httpStatus":null,"severity":"error","filePath":"clis/rednote/search.js","lineNumber":26,"sourceCode":"import { cli, Strategy } from '@jackwener/opencli/registry';\nimport { ArgumentError, AuthRequiredError, CommandExecutionError } from '@jackwener/opencli/errors';\nimport { buildScrollUntilJs, buildSearchExtractJs, noteIdToDate } from '../xiaohongshu/search.js';\nimport { unwrapEvaluateResult } from '../xiaohongshu/shared.js';\n\nfunction parseLimit(raw) {\n    const parsed = Number(raw);\n    if (!Number.isFinite(parsed) || !Number.isInteger(parsed)) {\n        throw new ArgumentError(`--limit must be an integer between 1 and 100, got ${JSON.stringify(raw)}`);\n    }\n    if (parsed < 1 || parsed > 100) {\n        throw new ArgumentError(`--limit must be between 1 and 100, got ${parsed}`);\n    }\n    return parsed;\n}\nfunction requireSearchRows(payload) {\n    const rows = unwrapEvaluateResult(payload);\n    if (!Array.isArray(rows)) {\n        throw new CommandExecutionError('Unexpected Rednote search extraction payload shape; expected an array of rows.');\n    }\n    return rows;\n}\n\n/**\n * Wait for search results or login wall using MutationObserver (max 5s).\n *\n * Differs from xiaohongshu by detecting a full-screen login modal instead\n * of (and as a fallback, alongside) the inline `登录后查看搜索结果` text.\n * The modal detector filters hidden / zero-area elements to avoid false\n * positives on background dialogs.\n */\nconst WAIT_FOR_CONTENT_JS = `\n  new Promise((resolve) => {\n    const hasLoginModal = () => {\n      const candidates = document.querySelectorAll(\n        '[class*=\"login-modal\"], [class*=\"LoginModal\"], [class*=\"login-container\"], [class*=\"LoginContainer\"], dialog[role=\"dialog\"]'\n      );","sourceCodeStart":8,"sourceCodeEnd":44,"githubUrl":"https://github.com/jackwener/OpenCLI/blob/49907e53dc3ade5c223ff0c4c2c2785687cec4e6/clis/rednote/search.js#L8-L44","documentation":"requireSearchRows unwraps the page.evaluate payload (via unwrapEvaluateResult) and demands an array of result rows. When the unwrapped value is not an array it throws CommandExecutionError, because the extraction script's output contract changed or the page returned an error/other shape.","triggerScenarios":"buildSearchExtractJs returning an object (e.g. {error:...}), unwrapEvaluateResult failing to unwrap a driver-specific wrapper, or the SPA replacing results with a login wall/error node mid-scroll.","commonSituations":"Rednote search hitting a login wall or CAPTCHA before results render, scrolling too fast so the script extracts a placeholder object, or a site frontend change altering the extract script's return type.","solutions":["Retry the command; if it persists, check the page manually for a login wall and authenticate","Add a wait-for-content step before extraction so rows are rendered first","Log the raw payload with unwrapEvaluateResult to see the actual shape returned","Update buildSearchExtractJs if the site's result markup/return shape changed"],"exampleFix":"// before\nconst rows = requireSearchRows(await page.evaluate(extractJs));\n// after\nawait page.evaluate(WAIT_FOR_CONTENT_JS); // ensure results or detect login wall first\nconst rows = requireSearchRows(await page.evaluate(extractJs));","handlingStrategy":"type-guard","validationCode":"const unwrapped = unwrapEvaluateResult(payload); if (!Array.isArray(unwrapped)) console.error('bad payload shape:', unwrapped);","typeGuard":"const isRowsPayload = (p) => Array.isArray(unwrapEvaluateResult(p));","tryCatchPattern":"try { return await searchRednote(query, limit); } catch (e) { if (e instanceof CommandExecutionError && /payload shape/.test(e.message)) { await waitForSearchContent(page); return searchRednote(query, limit); } throw e; }","preventionTips":["Wait for results (or detect a login wall) before running the extract script","Log the unwrapped payload when the shape check fails to diagnose site changes","Authenticate first so search does not return a wall/error object instead of rows"],"tags":["browser-automation","evaluate","payload-shape"],"backgroundTag":"unexpected-evaluate-response","analyzedSha":"49907e53dc3ade5c223ff0c4c2c2785687cec4e6","analyzedAt":"2026-08-29T08:14:47.543Z","schemaVersion":2},"datasetVersion":"2026-08-29T12:17:43.993Z"}