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

  1. Log the offending pages[index] to inspect which field is missing or renamed
  2. Check whether the illustration is a ugoira — use the ugoira metadata endpoint instead of /pages for those works
  3. Update the CLI/library to a version matching the current Pixiv pages schema
  4. Harden the mapping to fall back per-page (skip and warn) instead of failing the entire illustration
  5. 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

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

Related errors


AI-assisted analysis of jackwener/OpenCLI@49907e53dc (2026-08-29). Data as JSON: /api/errors/066914dbd09cf444. Report an issue: GitHub.