{"record":{"id":"307d11960a8d73e5","repo":"jackwener/OpenCLI","slug":"pixiv-novel-returned-malformed-series-metadata","errorCode":null,"errorMessage":"Pixiv novel returned malformed series metadata","messagePattern":"Pixiv novel returned malformed series metadata","errorType":"exception","errorClass":"CommandExecutionError","httpStatus":null,"severity":"error","filePath":"clis/pixiv/novel.js","lineNumber":33,"sourceCode":"  if (!body || Array.isArray(body) || typeof body !== 'object') {\n    throw new CommandExecutionError(`Pixiv novel ${id} returned malformed detail payload`);\n  }\n  const novelId = String(body.id ?? '').trim();\n  const title = String(body.title ?? '').trim();\n  const userName = String(body.userName ?? '').trim();\n  const userId = String(body.userId ?? '').trim();\n  if (!/^\\d+$/.test(novelId) || novelId !== id || !title || !userName || !/^\\d+$/.test(userId)) {\n    throw new CommandExecutionError(`Pixiv novel ${id} returned malformed detail payload`);\n  }\n  return { payload: body, identity: { novelId, title, userName, userId } };\n}\n\nexport function novelRowFromBody(body, id) {\n  const normalized = requireNovelBody(body, id);\n  const b = normalized.payload;\n  const identity = normalized.identity;\n  if (b.seriesNavData != null && (Array.isArray(b.seriesNavData) || typeof b.seriesNavData !== 'object')) {\n    throw new CommandExecutionError('Pixiv novel returned malformed series metadata');\n  }\n  const series = b.seriesNavData || {};\n  const seriesId = b.seriesId ?? series.seriesId ?? '';\n  const seriesTitle = b.seriesTitle ?? series.title ?? '';\n  if (seriesId !== '' && !/^\\d+$/.test(String(seriesId))) {\n    throw new CommandExecutionError('Pixiv novel returned malformed series ID');\n  }\n  if (seriesTitle !== '' && typeof seriesTitle !== 'string') {\n    throw new CommandExecutionError('Pixiv novel returned malformed series title');\n  }\n  const seriesOrder = series.order ?? b.seriesContentOrder ?? '';\n  if (seriesOrder !== '' && (!Number.isSafeInteger(seriesOrder) || seriesOrder < 1)) {\n    throw new CommandExecutionError('Pixiv novel returned malformed series order');\n  }\n  return {\n    novel_id: identity.novelId,\n    title: identity.title,\n    author: identity.userName,","sourceCodeStart":15,"sourceCodeEnd":51,"githubUrl":"https://github.com/jackwener/OpenCLI/blob/49907e53dc3ade5c223ff0c4c2c2785687cec4e6/clis/pixiv/novel.js#L15-L51","documentation":"novelRowFromBody validates the Pixiv /ajax/novel response before building a row. If seriesNavData is present but is an array or a non-object primitive (string, number, boolean), the payload's series metadata is structurally invalid, so the CLI throws CommandExecutionError instead of emitting bad data. This guards against upstream Pixiv API changes or proxy-injected garbage.","triggerScenarios":"The Pixiv AJAX novel endpoint returns seriesNavData as an array, a string, a number, or a boolean instead of a null/object (e.g. API schema change, HTML-interstitial or error JSON from a proxy/blocked request, cached/legacy payload shape).","commonSituations":"Pixiv changes the /ajax/novel/{id} response schema; a scraping proxy or auth wall returns an unexpected body; unit fixtures built from an old API version carry a seriesNavData array; callers pass a hand-crafted body into novelRowFromBody.","solutions":["Log the raw response body from pixivFetch to confirm what shape seriesNavData actually has","Update the CLI to the latest version so the parser matches the current Pixiv API schema","If a proxy/VPN is involved, retry the request with an authenticated session or different egress so Pixiv returns real JSON","As a caller, strip or null-out seriesNavData before invoking novelRowFromBody when you do not need series info"],"exampleFix":"// before (caller passing raw/legacy body)\nconst row = novelRowFromBody(legacyBody, id);\n// after\nif (Array.isArray(legacyBody.seriesNavData)) legacyBody.seriesNavData = null;\nconst row = novelRowFromBody(legacyBody, id);","handlingStrategy":"type-guard","validationCode":"function hasValidSeriesMeta(body) {\n  const s = body?.seriesNavData;\n  return s == null || (typeof s === 'object' && !Array.isArray(s));\n}\nif (!hasValidSeriesMeta(body)) throw new Error('unexpected seriesNavData shape');","typeGuard":"function isSeriesNavData(v) {\n  return v == null || (typeof v === 'object' && !Array.isArray(v));\n}","tryCatchPattern":"try {\n  const row = novelRowFromBody(body, id);\n} catch (e) {\n  if (e.message.includes('malformed series metadata')) {\n    console.warn('seriesNavData not an object; proceeding without series info');\n  } else throw e;\n}","preventionTips":["Never feed raw/legacy API bodies straight into novelRowFromBody without checking seriesNavData's type","Pin and update the CLI version when Pixiv schema changes are announced","Null-out seriesNavData in callers that do not need series data"],"tags":["pixiv","api-schema","validation","series-metadata"],"backgroundTag":"api-response-schema-mismatch","analyzedSha":"49907e53dc3ade5c223ff0c4c2c2785687cec4e6","analyzedAt":"2026-08-29T08:14:47.543Z","schemaVersion":2},"datasetVersion":"2026-08-29T12:17:43.993Z"}