{"record":{"id":"3297500a91ad3b03","repo":"jackwener/OpenCLI","slug":"pixiv-novel-returned-malformed-series-title","errorCode":null,"errorMessage":"Pixiv novel returned malformed series title","messagePattern":"Pixiv novel returned malformed series title","errorType":"exception","errorClass":"CommandExecutionError","httpStatus":null,"severity":"error","filePath":"clis/pixiv/novel.js","lineNumber":42,"sourceCode":"  }\n  return { payload: body, identity: { novelId, title, userName, userId } };\n}\n\nexport function novelRowFromBody(body, id) {\n  const normalized = requireNovelBody(body, id);\n  const b = normalized.payload;\n  const identity = normalized.identity;\n  if (b.seriesNavData != null && (Array.isArray(b.seriesNavData) || typeof b.seriesNavData !== 'object')) {\n    throw new CommandExecutionError('Pixiv novel returned malformed series metadata');\n  }\n  const series = b.seriesNavData || {};\n  const seriesId = b.seriesId ?? series.seriesId ?? '';\n  const seriesTitle = b.seriesTitle ?? series.title ?? '';\n  if (seriesId !== '' && !/^\\d+$/.test(String(seriesId))) {\n    throw new CommandExecutionError('Pixiv novel returned malformed series ID');\n  }\n  if (seriesTitle !== '' && typeof seriesTitle !== 'string') {\n    throw new CommandExecutionError('Pixiv novel returned malformed series title');\n  }\n  const seriesOrder = series.order ?? b.seriesContentOrder ?? '';\n  if (seriesOrder !== '' && (!Number.isSafeInteger(seriesOrder) || seriesOrder < 1)) {\n    throw new CommandExecutionError('Pixiv novel returned malformed series order');\n  }\n  return {\n    novel_id: identity.novelId,\n    title: identity.title,\n    author: identity.userName,\n    user_id: identity.userId,\n    series_id: seriesId === '' ? '' : String(seriesId),\n    series_title: seriesTitle || '',\n    series_order: seriesOrder,\n    words: optionalCount(b.wordCount, 'word count'),\n    characters: optionalCount(b.characterCount ?? b.textCount, 'character count'),\n    bookmarks: optionalCount(b.bookmarkCount, 'bookmark count') || 0,\n    likes: optionalCount(b.likeCount, 'like count') || 0,\n    views: optionalCount(b.viewCount, 'view count') || 0,","sourceCodeStart":24,"sourceCodeEnd":60,"githubUrl":"https://github.com/jackwener/OpenCLI/blob/49907e53dc3ade5c223ff0c4c2c2785687cec4e6/clis/pixiv/novel.js#L24-L60","documentation":"novelRowFromBody validates that a non-empty seriesTitle (from b.seriesTitle or seriesNavData.title) is a string. If it is a number, object, or array, the row builder refuses to emit a corrupted title column and throws CommandExecutionError.","triggerScenarios":"seriesTitle resolves to a non-string non-empty value — e.g. seriesNavData.title is an object containing localized variants {en, ja}, or seriesTitle was set to a number in a crafted/cached body.","commonSituations":"Pixiv experiments with localized title objects; third-party API mirrors return title as {raw:...}; test fixtures typed incorrectly; a custom normalization step assigned the wrong field.","solutions":["Dump the response to see the actual type of seriesNavData.title","If title is a localized object, pick one locale (e.g. .title || .en || .ja) and pass a plain string as seriesTitle","Update to the newest CLI version that may already unwrap localized titles","Fix upstream normalization code so seriesTitle is always a string or ''"],"exampleFix":"// before\nconst row = novelRowFromBody(body, id); // title is {en: \"...\", ja: \"...\"}\n// after\nconst t = body.seriesNavData?.title;\nif (t && typeof t === 'object') body.seriesTitle = t.en ?? t.ja ?? '';\nconst row = novelRowFromBody(body, id);","handlingStrategy":"type-guard","validationCode":"const t = body?.seriesNavData?.title;\nif (t != null && t !== '' && typeof t !== 'string') {\n  throw new Error('series title is not a string');\n}","typeGuard":"function isNonEmptyString(v) {\n  return typeof v === 'string' && v.length > 0;\n}","tryCatchPattern":"try {\n  const row = novelRowFromBody(body, id);\n} catch (e) {\n  if (e.message.includes('malformed series title')) {\n    const t = body.seriesNavData?.title;\n    body.seriesTitle = typeof t === 'object' ? (t.en ?? t.ja ?? '') : '';\n  } else throw e;\n}","preventionTips":["Unwrap localized title objects to a single string before building rows","Type-check third-party/mirrored API responses at the boundary","Avoid hand-built bodies with loosely typed title fields"],"tags":["pixiv","validation","series-title","type-error"],"backgroundTag":"api-response-schema-mismatch","analyzedSha":"49907e53dc3ade5c223ff0c4c2c2785687cec4e6","analyzedAt":"2026-08-29T08:14:47.543Z","schemaVersion":2},"datasetVersion":"2026-08-29T12:17:43.993Z"}