{"record":{"id":"a9ffd280acdd9255","repo":"jackwener/OpenCLI","slug":"weread-book-search-result-bookrank-is-missing-b","errorCode":null,"errorMessage":"WeRead book search result ${bookRank} is missing bookId","messagePattern":"WeRead book search result (.+?) is missing bookId","errorType":"exception","errorClass":"CommandExecutionError","httpStatus":null,"severity":"error","filePath":"clis/weread/book-search.js","lineNumber":239,"sourceCode":"        throw new CommandExecutionError('WeRead book search returned malformed books');\n    }\n    const books = data.books;\n    if (books.length === 0) {\n        throw new EmptyResultError('weread book-search', `No WeRead books found for \"${bookQuery}\"`);\n    }\n    if (bookRank > books.length) {\n        throw new ArgumentError(`book-rank must be <= ${books.length}`, `Only ${books.length} book search result(s) were returned for \"${bookQuery}\"`);\n    }\n    const bookInfo = books[bookRank - 1]?.bookInfo ?? {};\n    const selected = {\n        bookId: normalizeSearchText(bookInfo.bookId),\n        title: normalizeSearchText(bookInfo.title),\n        author: normalizeSearchText(bookInfo.author),\n        readerUrl: '',\n        chapters: [],\n    };\n    if (!selected.bookId) {\n        throw new CommandExecutionError(`WeRead book search result ${bookRank} is missing bookId`);\n    }\n    const htmlEntries = await loadSearchHtmlEntries(bookQuery);\n    selected.readerUrl = resolveReaderUrlForBook(selected, htmlEntries);\n    const readerMetadata = await loadReaderMetadata(selected.readerUrl);\n    return {\n        ...selected,\n        ...Object.fromEntries(Object.entries(readerMetadata ?? {}).filter(([, value]) => value != null && value !== '' && !(Array.isArray(value) && value.length === 0))),\n    };\n}\n\nasync function resolveBookTarget(target, bookRank) {\n    if (/^https?:\\/\\//i.test(target)) {\n        const readerUrl = parseWereadReaderUrl(target);\n        if (!readerUrl) {\n            throw new ArgumentError('book URL must be a https://weread.qq.com/web/reader/<id> URL');\n        }\n        const metadata = await loadReaderMetadata(readerUrl);\n        if (!metadata?.bookId) {","sourceCodeStart":221,"sourceCodeEnd":257,"githubUrl":"https://github.com/jackwener/OpenCLI/blob/49907e53dc3ade5c223ff0c4c2c2785687cec4e6/clis/weread/book-search.js#L221-L257","documentation":"After selecting books[bookRank - 1], searchBookByQuery normalizes bookInfo and requires a non-empty bookId since every downstream call (reader URL, metadata, highlights) depends on it. When WeRead returns a result object lacking bookId, the CLI refuses to continue with a CommandExecutionError rather than producing a broken book record.","triggerScenarios":"WeRead's search endpoint returns a result whose bookInfo exists but has no/empty bookId field (API shape change, ad/promotional entries, or restricted books), and the caller's rank selects that entry.","commonSituations":"WeRead changes its search response schema; the selected hit is a special (non-book) card such as an ad or bundle; rate-limiting or anti-bot responses return degraded payloads.","solutions":["Pick a different book-rank whose result has a valid bookId.","Verify the response schema against the current WeRead API and update the parser if the field was renamed.","Fall back to resolving the book by URL or numeric bookId instead of search."],"exampleFix":null,"handlingStrategy":"try-catch","validationCode":null,"typeGuard":"function hasBookId(r) {\n  return r != null && typeof r === 'object' && typeof r.bookId === 'string' && r.bookId.length > 0;\n}","tryCatchPattern":"try {\n  return await resolveBookTarget(target, rank);\n} catch (e) {\n  if (e instanceof CommandExecutionError && e.message.includes('missing bookId')) {\n    return resolveBookTarget(target, rank + 1); // try next search hit\n  }\n  throw e;\n}","preventionTips":["Try a different rank when one result is malformed.","Pin to numeric bookId for books you query repeatedly.","Watch for WeRead API schema changes in CI tests."],"tags":["api-schema","search","weread"],"backgroundTag":"api-schema-changed","analyzedSha":"49907e53dc3ade5c223ff0c4c2c2785687cec4e6","analyzedAt":"2026-08-29T08:14:47.543Z","schemaVersion":2},"datasetVersion":"2026-08-29T12:17:43.993Z"}