jackwener/OpenCLI · error · CommandExecutionError
Pixiv pages API returned malformed payload
Error message
Pixiv pages API returned malformed payload
What it means
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.
Source
Thrown at clis/pixiv/bookmark-download.js:55
try {
url = new URL(value);
} catch {
throw new CommandExecutionError(`${label} returned a malformed image URL`);
}
const extension = path.extname(url.pathname).toLowerCase();
const contentType = IMAGE_CONTENT_TYPES.get(extension);
if (url.protocol !== 'https:' || url.hostname !== 'i.pximg.net' || url.username || url.password || url.port || !contentType) {
throw new CommandExecutionError(`${label} returned an untrusted Pixiv image URL`);
}
return { url: url.href, extension, contentType };
}
async function prepareIllustPlan(page, row, outputRoot) {
const pages = await pixivFetch(page, `/ajax/illust/${row.illust_id}/pages`, {
notFoundMsg: `Illustration not found: ${row.illust_id}`,
});
if (!Array.isArray(pages)) {
throw new CommandExecutionError('Pixiv pages API returned malformed payload');
}
if (pages.length === 0) {
throw new EmptyResultError('pixiv bookmark-download', `No images found for illustration ${row.illust_id}.`);
}
const files = pages.map((entry, index) => {
if (!entry || Array.isArray(entry) || typeof entry !== 'object' || !entry.urls || Array.isArray(entry.urls) || typeof entry.urls !== 'object') {
throw new CommandExecutionError(`Pixiv illustration ${row.illust_id} returned malformed page ${index + 1}`);
}
const parsed = parsePixivImageUrl(entry.urls.original || entry.urls.regular, `Pixiv illustration ${row.illust_id} page ${index + 1}`);
return {
...parsed,
filename: `${row.illust_id}_p${index}${parsed.extension}`,
};
});
const finalPath = path.join(outputRoot, 'illust', row.illust_id);
if (pixivPathEntryExists(finalPath)) {
throw new CommandExecutionError(`Refusing to overwrite existing Pixiv download: ${finalPath}`);
}View on GitHub (pinned to 49907e53dc)
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
Example fix
// before
const pages = await pixivFetch(page, `/ajax/illust/${row.illust_id}/pages`, {...});
if (!Array.isArray(pages)) throw new CommandExecutionError('Pixiv pages API returned malformed payload');
// after
const payload = await pixivFetch(page, `/ajax/illust/${row.illust_id}/pages`, {...});
const pages = Array.isArray(payload) ? payload : payload?.body?.pages ?? payload?.body;
if (!Array.isArray(pages)) throw new CommandExecutionError(`Pixiv pages API returned malformed payload: ${JSON.stringify(payload).slice(0, 200)}`); Defensive patterns
Strategy: type-guard
Validate before calling
// Verify the endpoint shape before relying on it
const payload = await pixivFetch(page, `/ajax/illust/${row.illust_id}/pages`, {});
if (!Array.isArray(payload)) throw new Error('unexpected pages payload: ' + JSON.stringify(payload).slice(0, 200)); Type guard
function isPixivPagesArray(v) {
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));
} Try / catch
try {
await bookmarkDownload(row);
} catch (err) {
if (err instanceof CommandExecutionError && /malformed payload/.test(err.message)) {
console.error(`Pixiv API shape changed for illust ${row.illust_id}; check payload and library version`);
} else { throw err; }
} Prevention
- 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
When it happens
Trigger: 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.
Common situations: 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.
Understand the failure class
- Parsing and encoding errors: unexpected token, malformed input — why parsers reject input and how to find the real culprit.
Related errors
- ${label} returned HTTP ${resp.status}: ${summarizeApiError(p
- No user message found in request
- arXiv API HTTP ${resp.status}
- 获取视频分P信息失败: ${error?.message || error}
- 获取视频信息失败: ${err?.message || err}
AI-assisted analysis of jackwener/OpenCLI@49907e53dc (2026-08-29).
Data as JSON: /api/errors/bc8470166f99cfe7.
Report an issue: GitHub.