{"record":{"id":"066914dbd09cf444","repo":"jackwener/OpenCLI","slug":"pixiv-illustration-row-illust-id-returned-malfo","errorCode":null,"errorMessage":"Pixiv illustration ${row.illust_id} returned malformed page ${index + 1}","messagePattern":"Pixiv illustration (.+?) returned malformed page (.+?)","errorType":"exception","errorClass":"CommandExecutionError","httpStatus":null,"severity":"error","filePath":"clis/pixiv/bookmark-download.js","lineNumber":62,"sourceCode":"  if (url.protocol !== 'https:' || url.hostname !== 'i.pximg.net' || url.username || url.password || url.port || !contentType) {\n    throw new CommandExecutionError(`${label} returned an untrusted Pixiv image URL`);\n  }\n  return { url: url.href, extension, contentType };\n}\n\nasync function prepareIllustPlan(page, row, outputRoot) {\n  const pages = await pixivFetch(page, `/ajax/illust/${row.illust_id}/pages`, {\n    notFoundMsg: `Illustration not found: ${row.illust_id}`,\n  });\n  if (!Array.isArray(pages)) {\n    throw new CommandExecutionError('Pixiv pages API returned malformed payload');\n  }\n  if (pages.length === 0) {\n    throw new EmptyResultError('pixiv bookmark-download', `No images found for illustration ${row.illust_id}.`);\n  }\n  const files = pages.map((entry, index) => {\n    if (!entry || Array.isArray(entry) || typeof entry !== 'object' || !entry.urls || Array.isArray(entry.urls) || typeof entry.urls !== 'object') {\n      throw new CommandExecutionError(`Pixiv illustration ${row.illust_id} returned malformed page ${index + 1}`);\n    }\n    const parsed = parsePixivImageUrl(entry.urls.original || entry.urls.regular, `Pixiv illustration ${row.illust_id} page ${index + 1}`);\n    return {\n      ...parsed,\n      filename: `${row.illust_id}_p${index}${parsed.extension}`,\n    };\n  });\n  const finalPath = path.join(outputRoot, 'illust', row.illust_id);\n  if (pixivPathEntryExists(finalPath)) {\n    throw new CommandExecutionError(`Refusing to overwrite existing Pixiv download: ${finalPath}`);\n  }\n  const createdDirs = [];\n  for (let cursor = path.dirname(finalPath); !fs.existsSync(cursor); cursor = path.dirname(cursor)) {\n    createdDirs.push(cursor);\n    if (path.dirname(cursor) === cursor) break;\n  }\n  return { kind: 'illust', illustId: row.illust_id, finalPath, files, createdDirs };\n}","sourceCodeStart":44,"sourceCodeEnd":80,"githubUrl":"https://github.com/jackwener/OpenCLI/blob/49907e53dc3ade5c223ff0c4c2c2785687cec4e6/clis/pixiv/bookmark-download.js#L44-L80","documentation":"Each element of the pages array must be an object with a urls object containing image URLs. If any page entry is null, an array, or lacks a valid urls object, prepareIllustPlan throws CommandExecutionError naming the offending page index. This validates the per-page schema before attempting to parse the image URL.","triggerScenarios":"During pages.map(), a page entry is null/an array/missing urls, or entry.urls is an array or not an object — e.g. Pixiv omits the urls field for certain work types, localizes a different field name, or the payload is partially corrupted.","commonSituations":"Pixiv schema drift for newer work types (ugoira, multi-page comics with mixed entries); intermittent truncated responses; passing a stubbed/mocked API response in tests that lacks urls; proxy or CDN returning partial JSON.","solutions":["Log the offending pages[index] to inspect which field is missing or renamed","Check whether the illustration is a ugoira — use the ugoira metadata endpoint instead of /pages for those works","Update the CLI/library to a version matching the current Pixiv pages schema","Harden the mapping to fall back per-page (skip and warn) instead of failing the entire illustration","Re-run the download; if intermittent, it may be a truncated network response"],"exampleFix":"// before\nif (!entry || Array.isArray(entry) || typeof entry !== 'object' || !entry.urls || Array.isArray(entry.urls) || typeof entry.urls !== 'object') {\n  throw new CommandExecutionError(`Pixiv illustration ${row.illust_id} returned malformed page ${index + 1}`);\n}\n// after\nconst ok = (e) => !!e && !Array.isArray(e) && typeof e === 'object' && e.urls && !Array.isArray(e.urls) && typeof e.urls === 'object';\nconst files = pages.flatMap((entry, index) => {\n  if (!ok(entry)) { console.warn(`skipping malformed page ${index + 1} of ${row.illust_id}`); return []; }\n  return [{ ...parsePixivImageUrl(entry.urls.original || entry.urls.regular, `page ${index+1}`), filename: `${row.illust_id}_p${index}` }];\n});","handlingStrategy":"validation","validationCode":"const pages = await pixivFetch(page, `/ajax/illust/${row.illust_id}/pages`, {});\nconst bad = Array.isArray(pages) ? pages.findIndex(e => !e || typeof e !== 'object' || Array.isArray(e) || !e.urls || typeof e.urls !== 'object' || Array.isArray(e.urls)) : -1;\nif (bad !== -1) console.warn(`page ${bad + 1} of ${row.illust_id} is malformed`);","typeGuard":"function isPixivPageEntry(entry) {\n  return Boolean(entry) && typeof entry === 'object' && !Array.isArray(entry)\n    && entry.urls !== null && typeof entry.urls === 'object' && !Array.isArray(entry.urls)\n    && typeof (entry.urls.original || entry.urls.regular) === 'string';\n}","tryCatchPattern":"try {\n  await bookmarkDownload(row);\n} catch (err) {\n  if (err instanceof CommandExecutionError && /malformed page \\d+/.test(err.message)) {\n    console.error(`Schema issue on ${err.message}; inspect payload and update parser`);\n  } else { throw err; }\n}","preventionTips":["Unit-test the pages mapper against current real API payloads, not stale fixtures","Include ugoira/edge-case works in test fixtures since their entries differ","Log the full page entry in the error message to speed up schema-drift diagnosis","Prefer per-entry fallback (skip + warn) in custom scripts over failing the whole illustration"],"tags":["api","validation","schema"],"backgroundTag":"unexpected-api-response-shape","analyzedSha":"49907e53dc3ade5c223ff0c4c2c2785687cec4e6","analyzedAt":"2026-08-29T08:14:47.543Z","schemaVersion":2},"datasetVersion":"2026-08-29T12:17:43.993Z"}