{"record":{"id":"bc8470166f99cfe7","repo":"jackwener/OpenCLI","slug":"pixiv-pages-api-returned-malformed-payload","errorCode":null,"errorMessage":"Pixiv pages API returned malformed payload","messagePattern":"Pixiv pages API returned malformed payload","errorType":"exception","errorClass":"CommandExecutionError","httpStatus":null,"severity":"error","filePath":"clis/pixiv/bookmark-download.js","lineNumber":55,"sourceCode":"  try {\n    url = new URL(value);\n  } catch {\n    throw new CommandExecutionError(`${label} returned a malformed image URL`);\n  }\n  const extension = path.extname(url.pathname).toLowerCase();\n  const contentType = IMAGE_CONTENT_TYPES.get(extension);\n  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  }","sourceCodeStart":37,"sourceCodeEnd":73,"githubUrl":"https://github.com/jackwener/OpenCLI/blob/49907e53dc3ade5c223ff0c4c2c2785687cec4e6/clis/pixiv/bookmark-download.js#L37-L73","documentation":"prepareIllustPlan calls the Pixiv /ajax/illust/{id}/pages endpoint via pixivFetch and expects the response body to be an array of page entries. When the JSON body is not an array (e.g. an object, string, or null was returned), the library throws CommandExecutionError('Pixiv pages API returned malformed payload') to stop before mapping over pages. This guards against Pixiv changing its internal API shape or returning an error envelope instead of the pages list.","triggerScenarios":"pixivFetch resolves successfully but the /ajax/illust/{illust_id}/pages body is not an array — for example Pixiv returns an object envelope, an HTML/login page parsed as a string, or a null body when the illustration is restricted or the API contract changes.","commonSituations":"Pixiv A/B testing or an internal API change alters the response shape; an authenticated session expired so the endpoint returns an error object instead of pages; hitting the endpoint for an illustration type (e.g. ugoira or deleted works) that returns a different payload; scraping without proper cookies/headers causing a soft-error JSON object.","solutions":["Log the raw response from /ajax/illust/{id}/pages to see the actual payload shape before assuming a schema break","Re-authenticate: refresh the Pixiv PHPSESSID/cookie used by pixivFetch, since expired sessions often yield error envelopes instead of page arrays","Verify the illustration id is a normal illust (not ugoira/novel/deleted); ugoira works use a different endpoint","Check for CLI/library updates that track Pixiv's current API response format","If Pixiv changed the schema, unwrap the new envelope (e.g. body.pages or data.body) before the Array.isArray check"],"exampleFix":"// before\nconst pages = await pixivFetch(page, `/ajax/illust/${row.illust_id}/pages`, {...});\nif (!Array.isArray(pages)) throw new CommandExecutionError('Pixiv pages API returned malformed payload');\n// after\nconst payload = await pixivFetch(page, `/ajax/illust/${row.illust_id}/pages`, {...});\nconst pages = Array.isArray(payload) ? payload : payload?.body?.pages ?? payload?.body;\nif (!Array.isArray(pages)) throw new CommandExecutionError(`Pixiv pages API returned malformed payload: ${JSON.stringify(payload).slice(0, 200)}`);","handlingStrategy":"type-guard","validationCode":"// Verify the endpoint shape before relying on it\nconst payload = await pixivFetch(page, `/ajax/illust/${row.illust_id}/pages`, {});\nif (!Array.isArray(payload)) throw new Error('unexpected pages payload: ' + JSON.stringify(payload).slice(0, 200));","typeGuard":"function isPixivPagesArray(v) {\n  return Array.isArray(v) && v.length > 0 && v.every(e => e && typeof e === 'object' && !Array.isArray(e) && e.urls && typeof e.urls === 'object' && !Array.isArray(e.urls));\n}","tryCatchPattern":"try {\n  await bookmarkDownload(row);\n} catch (err) {\n  if (err instanceof CommandExecutionError && /malformed payload/.test(err.message)) {\n    console.error(`Pixiv API shape changed for illust ${row.illust_id}; check payload and library version`);\n  } else { throw err; }\n}","preventionTips":["Pin and update the CLI/library so it tracks Pixiv's current internal API format","Log raw API payloads in debug mode to detect schema drift early","Keep session cookies fresh; expired sessions often return non-array error envelopes","Validate the illustration type (illust vs ugoira) before calling /pages"],"tags":["api","validation","network"],"backgroundTag":"unexpected-api-response-shape","analyzedSha":"49907e53dc3ade5c223ff0c4c2c2785687cec4e6","analyzedAt":"2026-08-29T08:14:47.543Z","schemaVersion":2},"datasetVersion":"2026-08-29T12:17:43.993Z"}