{"record":{"id":"37ab44d564d1741a","repo":"jackwener/OpenCLI","slug":"pixiv-user-novel-item-returned-malformed-label","errorCode":null,"errorMessage":"Pixiv user novel item returned malformed ${label}","messagePattern":"Pixiv user novel item returned malformed (.+?)","errorType":"exception","errorClass":"CommandExecutionError","httpStatus":null,"severity":"error","filePath":"clis/pixiv/novels.js","lineNumber":16,"sourceCode":"import { cli, Strategy } from '@jackwener/opencli/registry';\nimport { ArgumentError, CommandExecutionError } from '@jackwener/opencli/errors';\nimport {\n  BATCH_SIZE,\n  normalizePixivPositiveInteger,\n  pixivFetch,\n  requirePixivId,\n  requirePixivPayloadObject,\n  requirePixivString,\n} from './utils.js';\nimport { dateOnly, tagsToString } from './bookmark-utils.js';\n\nfunction optionalCount(value, label, fallback = '') {\n  if (value == null || value === '') return fallback;\n  if (!Number.isSafeInteger(value) || value < 0) {\n    throw new CommandExecutionError(`Pixiv user novel item returned malformed ${label}`);\n  }\n  return value;\n}\n\nfunction userNovelRow(work, rank, expectedId) {\n  const item = requirePixivPayloadObject(work, 'Pixiv user novel item');\n  const id = requirePixivId(item.id, 'Pixiv user novel item');\n  if (id !== expectedId) {\n    throw new CommandExecutionError(`Pixiv user novels returned mismatched novel detail payload for ${expectedId}`);\n  }\n  const title = requirePixivString(item.title, 'Pixiv user novel item');\n  return {\n    rank,\n    title,\n    novel_id: id,\n    words: optionalCount(item.wordCount, 'word count'),\n    characters: optionalCount(item.textCount ?? item.characterCount, 'character count'),\n    bookmarks: optionalCount(item.bookmarkCount, 'bookmark count', 0),","sourceCodeStart":1,"sourceCodeEnd":34,"githubUrl":"https://github.com/jackwener/OpenCLI/blob/49907e53dc3ade5c223ff0c4c2c2785687cec4e6/clis/pixiv/novels.js#L1-L34","documentation":"optionalCount validates that a numeric count field on a Pixiv user-novel item is a safe, non-negative integer before the CLI renders it. When the value is present but not a safe non-negative integer (e.g. a string, float, negative number, or boolean), the library throws CommandExecutionError to fail fast on malformed upstream Pixiv API payloads rather than emit corrupted ranking output. It only fires when the value is neither null nor empty string, since those fall back to the fallback value.","triggerScenarios":"Pixiv's user novel detail endpoint returns a numeric field (like total_words, marker_count, or series count) as a string ('1234'), a float (12.5), a negative number, or another non-safe-integer type inside a works[] item passed to userNovelRow.","commonSituations":"Pixiv API schema changes or undocumented field types (numbers serialized as strings); proxies/caches re-serializing JSON with type coercion; a user-supplied expected id or scraped profile payload feeding the CLI with hand-edited data; new API fields no longer matching the CLI's assumptions.","solutions":["Log the raw payload item and the offending field to identify which count field has the wrong type","Coerce the value before calling: Number.isFinite(+value) ? Number(value) : fallback, or pass value through a Number() cast if the API legitimately sends strings","Upgrade/patch the CLI's optionalCount to accept numeric strings via /^\\d+$/ before Number.isSafeInteger","If Pixiv itself is returning malformed data, retry the request later or report the schema drift"],"exampleFix":"// before\nconst words = optionalCount(item.total_words, 'total_words');\n// after\nconst rawWords = item.total_words;\nconst words = optionalCount(\n  typeof rawWords === 'string' && /^\\d+$/.test(rawWords) ? Number(rawWords) : rawWords,\n  'total_words'\n);","handlingStrategy":"validation","validationCode":"function isNonNegSafeInt(v) { return typeof v === 'number' && Number.isSafeInteger(v) && v >= 0; }\nif (item.total_words != null && item.total_words !== '' && !isNonNegSafeInt(item.total_words)) {\n  throw new Error('Skipping novel: malformed total_words');\n}","typeGuard":"function isCount(v) {\n  return typeof v === 'number' && Number.isSafeInteger(v) && v >= 0;\n}\nfunction asCount(v, fallback = 0) {\n  if (typeof v === 'string' && /^\\d+$/.test(v)) return Number(v);\n  return isCount(v) ? v : fallback;\n}","tryCatchPattern":"try {\n  row = userNovelRow(work, rank, expectedId);\n} catch (err) {\n  if (String(err.message).includes('malformed')) {\n    console.warn(`Skipping rank ${rank}: ${err.message}`);\n    return null;\n  }\n  throw err;\n}","preventionTips":["Coerce numeric API fields with Number() and validate before use","Prefer explicit typeof checks over truthiness for numeric payload fields","Wrap each row render so one malformed item does not abort the whole ranking run","Pin and test against recorded Pixiv API fixtures to catch schema drift early"],"tags":["pixiv","cli","validation","api-payload"],"backgroundTag":"malformed-api-payload","analyzedSha":"49907e53dc3ade5c223ff0c4c2c2785687cec4e6","analyzedAt":"2026-08-29T08:14:47.543Z","schemaVersion":2},"datasetVersion":"2026-08-29T12:17:43.993Z"}