jackwener/OpenCLI · error · CommandExecutionError
Pixiv illustration ${row.illust_id} returned malformed page
Error message
Pixiv illustration ${row.illust_id} returned malformed page ${index + 1} What it means
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.
Source
Thrown at clis/pixiv/bookmark-download.js:62
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}`);
}
const createdDirs = [];
for (let cursor = path.dirname(finalPath); !fs.existsSync(cursor); cursor = path.dirname(cursor)) {
createdDirs.push(cursor);
if (path.dirname(cursor) === cursor) break;
}
return { kind: 'illust', illustId: row.illust_id, finalPath, files, createdDirs };
}View on GitHub (pinned to 49907e53dc)
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
Example fix
// before
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}`);
}
// after
const ok = (e) => !!e && !Array.isArray(e) && typeof e === 'object' && e.urls && !Array.isArray(e.urls) && typeof e.urls === 'object';
const files = pages.flatMap((entry, index) => {
if (!ok(entry)) { console.warn(`skipping malformed page ${index + 1} of ${row.illust_id}`); return []; }
return [{ ...parsePixivImageUrl(entry.urls.original || entry.urls.regular, `page ${index+1}`), filename: `${row.illust_id}_p${index}` }];
}); Defensive patterns
Strategy: validation
Validate before calling
const pages = await pixivFetch(page, `/ajax/illust/${row.illust_id}/pages`, {});
const 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;
if (bad !== -1) console.warn(`page ${bad + 1} of ${row.illust_id} is malformed`); Type guard
function isPixivPageEntry(entry) {
return Boolean(entry) && typeof entry === 'object' && !Array.isArray(entry)
&& entry.urls !== null && typeof entry.urls === 'object' && !Array.isArray(entry.urls)
&& typeof (entry.urls.original || entry.urls.regular) === 'string';
} Try / catch
try {
await bookmarkDownload(row);
} catch (err) {
if (err instanceof CommandExecutionError && /malformed page \d+/.test(err.message)) {
console.error(`Schema issue on ${err.message}; inspect payload and update parser`);
} else { throw err; }
} Prevention
- 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
When it happens
Trigger: 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.
Common situations: 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.
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
- Bilibili user search returned malformed result for ${input}
- ${label} returned an unexpected payload shape; expected an o
- ${label} returned an unexpected payload shape; expected an a
- ${label} did not include a stable ${field}.
- No user message found in request
AI-assisted analysis of jackwener/OpenCLI@49907e53dc (2026-08-29).
Data as JSON: /api/errors/066914dbd09cf444.
Report an issue: GitHub.