{"record":{"id":"04e4680d1f9b2999","repo":"jackwener/OpenCLI","slug":"pixiv-bookmarks-returned-malformed-payload","errorCode":null,"errorMessage":"Pixiv bookmarks returned malformed payload","messagePattern":"Pixiv bookmarks returned malformed payload","errorType":"exception","errorClass":"CommandExecutionError","httpStatus":null,"severity":"error","filePath":"clis/pixiv/bookmark-utils.js","lineNumber":55,"sourceCode":"      throw new CommandExecutionError('Pixiv item returned malformed tag');\n    }\n    return value.trim();\n  }).join(', ');\n}\n\nfunction optionalCount(value, label, fallback = '') {\n  if (value == null || value === '') return fallback;\n  if (!Number.isSafeInteger(value) || value < 0) {\n    throw new CommandExecutionError(`Pixiv bookmark item returned malformed ${label}`);\n  }\n  return value;\n}\n\nexport function normalizeBookmarkWorks(body) {\n  if (Array.isArray(body)) return body;\n  if (Array.isArray(body?.works)) return body.works;\n  if (body?.works && typeof body.works === 'object') return Object.values(body.works);\n  throw new CommandExecutionError('Pixiv bookmarks returned malformed payload');\n}\n\nexport function bookmarkRow(work, index, type, bookmarkOwnerId) {\n  const item = requirePixivPayloadObject(work, 'Pixiv bookmark item');\n  const isNovel = type === 'novel';\n  const id = requirePixivId(item.id ?? (isNovel ? item.novelId : item.illustId), 'Pixiv bookmark item');\n  const title = requirePixivString(item.title ?? item.illustTitle, 'Pixiv bookmark item');\n  const author = requirePixivString(item.userName ?? item.user_name, 'Pixiv bookmark item');\n  const userId = requirePixivId(item.userId ?? item.user_id, 'Pixiv bookmark item');\n  return {\n    rank: index + 1,\n    type,\n    bookmark_owner_id: bookmarkOwnerId,\n    title,\n    author,\n    user_id: userId,\n    illust_id: isNovel ? '' : id,\n    novel_id: isNovel ? id : '',","sourceCodeStart":37,"sourceCodeEnd":73,"githubUrl":"https://github.com/jackwener/OpenCLI/blob/49907e53dc3ade5c223ff0c4c2c2785687cec4e6/clis/pixiv/bookmark-utils.js#L37-L73","documentation":"normalizeBookmarkWorks validates the JSON returned by Pixiv's /ajax/user/<id>/.../bookmarks endpoint before rows are built. It accepts a plain array, an object with a `works` array, or an object with an object-valued `works` map; anything else is a payload shape the library cannot safely iterate, so it throws this CommandExecutionError to surface upstream/API drift instead of producing garbage rows.","triggerScenarios":"Pixiv's ajax endpoint returns an HTML login page, an error JSON like {\"error\":true,...}, an empty body, or a new schema where bookmarks live under a different key — any body that is neither an array nor an object with a usable `works` field.","commonSituations":"Expired or missing Pixiv login session so the ajax call returns a redirect/login page; Pixiv A/B-testing a new bookmark response schema; rate-limit or error envelope JSON returned with HTTP 200; scraping a region where the endpoint is gated.","solutions":["Check Pixiv login state: re-authenticate the page/session, since an unauthenticated ajax response is the most common cause of a non-works payload.","Log the raw body before calling normalizeBookmarkWorks (or catch and inspect body) to see the actual schema returned.","Update the normalizer to handle the new response shape (e.g. accept body.bookmarks or body.body.works) if Pixiv changed its schema.","Retry after a delay if the payload was a rate-limit/error envelope; use pixivFetch's error handling / notFoundMsg paths.","Pin/verify the library version matches the current Pixiv API behavior; upgrade if a fix exists."],"exampleFix":"// before\nconst body = await pixivFetch(page, path, {...});\nconst works = normalizeBookmarkWorks(body);\n// after\nconst body = await pixivFetch(page, path, {...});\nlet works;\ntry {\n  works = normalizeBookmarkWorks(body);\n} catch (e) {\n  if (!body?.error) throw e;\n  throw new CommandExecutionError(`Pixiv bookmarks request failed: ${body.message ?? 'unauthenticated or rate-limited'}`);\n}","handlingStrategy":"type-guard","validationCode":"function isUsableBookmarksBody(body) {\n  return Array.isArray(body) ||\n    Array.isArray(body?.works) ||\n    (body?.works && typeof body.works === 'object');\n}\nif (!isUsableBookmarksBody(body)) {\n  console.error('Unexpected bookmarks payload:', JSON.stringify(body).slice(0, 500));\n}","typeGuard":"function isUsableBookmarksBody(body) {\n  return Array.isArray(body) ||\n    (body !== null && typeof body === 'object' &&\n      (Array.isArray(body.works) || (body.works !== null && typeof body.works === 'object')));\n}","tryCatchPattern":"try {\n  const works = normalizeBookmarkWorks(body);\n  // ...build rows\n} catch (e) {\n  if (e.message.includes('malformed payload')) {\n    // inspect/log raw body, check login state, then rethrow or degrade gracefully\n    throw new Error(`Bookmarks unavailable (payload shape: ${typeof body}); check Pixiv login/session`, { cause: e });\n  }\n  throw e;\n}","preventionTips":["Keep the Pixiv session authenticated; most malformed payloads come from login-page or error responses.","Log the raw ajax body on failure to detect Pixiv schema changes early.","Wrap normalizeBookmarkWorks and degrade gracefully (return empty rows + warning) for read-only tooling.","Add a smoke test that runs fetchCurrentBookmarks and asserts the payload guard passes.","Pin and periodically update the library against Pixiv API drift."],"tags":["pixiv","payload-validation","api-schema","scraping"],"backgroundTag":"schema-validation-failed","analyzedSha":"49907e53dc3ade5c223ff0c4c2c2785687cec4e6","analyzedAt":"2026-08-29T08:14:47.543Z","schemaVersion":2},"datasetVersion":"2026-08-29T12:17:43.993Z"}