{"record":{"id":"2861cda0e0c1e0c8","repo":"jackwener/OpenCLI","slug":"commandname-malformed-comment-row-text","errorCode":null,"errorMessage":"${commandName}: malformed comment row text","messagePattern":"(.+?): malformed comment row text","errorType":"exception","errorClass":"CommandExecutionError","httpStatus":null,"severity":"error","filePath":"clis/xiaohongshu/comments.js","lineNumber":117,"sourceCode":"        if (!urls.includes(href))\n            urls.push(href);\n    }\n    return urls;\n}\n\nexport function normalizeCommentRows(value, commandName = 'xiaohongshu/comments') {\n    if (value == null)\n        return [];\n    if (!Array.isArray(value)) {\n        throw new CommandExecutionError(`${commandName}: malformed comments payload`);\n    }\n    return value.map((row, index) => {\n        if (!row || typeof row !== 'object' || Array.isArray(row)) {\n            throw new CommandExecutionError(`${commandName}: malformed comment row at index ${index}`);\n        }\n        const text = normalizeOptionalString(row.text, 'text', commandName);\n        if (!text) {\n            throw new CommandExecutionError(`${commandName}: malformed comment row text`);\n        }\n        const likes = Number(row.likes);\n        if (!Number.isInteger(likes) || likes < 0) {\n            throw new CommandExecutionError(`${commandName}: malformed comment row likes`);\n        }\n        if (typeof row.is_reply !== 'boolean') {\n            throw new CommandExecutionError(`${commandName}: malformed comment row is_reply`);\n        }\n        return {\n            author: normalizeOptionalString(row.author, 'author', commandName),\n            authorHrefRaw: normalizeOptionalString(row.authorHrefRaw, 'authorHrefRaw', commandName),\n            text,\n            likes,\n            time: normalizeOptionalString(row.time, 'time', commandName),\n            is_reply: row.is_reply,\n            reply_to: normalizeOptionalString(row.reply_to, 'reply_to', commandName),\n            images: normalizeCommentImages(row.images, commandName),\n        };","sourceCodeStart":99,"sourceCodeEnd":135,"githubUrl":"https://github.com/jackwener/OpenCLI/blob/49907e53dc3ade5c223ff0c4c2c2785687cec4e6/clis/xiaohongshu/comments.js#L99-L135","documentation":"normalizeCommentRows validates each row returned by the in-page comments extractor. When a row is an object but its text field normalizes to empty/missing (normalizeOptionalString returns falsy), the command rejects it because a comment without text is useless downstream. The commandName ('xiaohongshu/comments') is interpolated into the message.","triggerScenarios":"The page.evaluate extractor returned a results array containing a row object whose text is null, undefined, an empty string, or only whitespace (normalizeOptionalString trims).","commonSituations":"Xiaohongshu DOM changes so the extractor's text selector matches an empty node; comments deleted by the author leave empty placeholders; custom/patched extract JS passed via buildCommentsExtractJs emits rows without text.","solutions":["Inspect data.results from the extractor and drop rows with empty/whitespace text before normalizing","Update buildCommentsExtractJs selectors to correctly capture comment text for the current Xiaohongshu DOM","Filter malformed rows at the call site in normalizeCommentRows (skip instead of throw) if partial results are acceptable","Retry the scrape — a transient render may have produced empty text nodes"],"exampleFix":"// before\nconst text = normalizeOptionalString(row.text, 'text', commandName);\nif (!text) {\n    throw new CommandExecutionError(`${commandName}: malformed comment row text`);\n}\n// after\nconst text = normalizeOptionalString(row.text, 'text', commandName);\nif (!text) {\n    return null; // filter out empty rows instead of failing the whole command\n}","handlingStrategy":"validation","validationCode":"function isValidCommentRows(rows) {\n  return Array.isArray(rows) && rows.every(r => r && typeof r === 'object'\n    && typeof r.text === 'string' && r.text.trim() !== '');\n}\nif (!isValidCommentRows(data.results)) throw new Error('extractor returned rows without text');","typeGuard":"const hasText = (row) => typeof row === 'object' && row !== null\n  && typeof row.text === 'string' && row.text.trim().length > 0;","tryCatchPattern":"try {\n  const comments = await cli.comments(noteUrl, { limit: 20 });\n} catch (err) {\n  if (err.message.includes('malformed comment row text')) {\n    // retry or fall back to partial handling\n  } else throw err;\n}","preventionTips":["Keep buildCommentsExtractJs selectors updated against the live Xiaohongshu DOM","Filter empty-text rows in the extractor before returning","Add integration tests with real page fixtures"],"tags":["validation","scraping","dom-changes","data-shape"],"backgroundTag":"schema-validation-failed","analyzedSha":"49907e53dc3ade5c223ff0c4c2c2785687cec4e6","analyzedAt":"2026-08-29T08:14:47.543Z","schemaVersion":2},"datasetVersion":"2026-08-29T12:17:43.993Z"}