{"record":{"id":"5c2e0283a369ab27","repo":"jackwener/OpenCLI","slug":"weread-search-api-returned-an-unreadable-books-pay","errorCode":null,"errorMessage":"WeRead search API returned an unreadable books payload","messagePattern":"WeRead search API returned an unreadable books payload","errorType":"exception","errorClass":"CommandExecutionError","httpStatus":null,"severity":"error","filePath":"clis/weread/search.js","lineNumber":152,"sourceCode":"    name: 'search',\n    access: 'read',\n    description: 'Search books on WeRead',\n    domain: 'weread.qq.com',\n    strategy: Strategy.PUBLIC,\n    browser: false,\n    args: [\n        { name: 'query', positional: true, required: true, help: 'Search keyword' },\n        { name: 'limit', type: 'int', default: 10, help: 'Max results' },\n    ],\n    columns: ['rank', 'title', 'author', 'bookId', 'url'],\n    func: async (args) => {\n        const [data, htmlEntries] = await Promise.all([\n            fetchWebApi('/search/global', { keyword: args.query }),\n            loadSearchHtmlEntries(String(args.query ?? '')),\n        ]);\n        const books = data?.books ?? [];\n        if (!Array.isArray(books)) {\n            throw new CommandExecutionError('WeRead search API returned an unreadable books payload');\n        }\n        if (books.length === 0) {\n            throw new EmptyResultError('weread search', `No books were returned for query ${args.query}.`);\n        }\n        const { exactQueues, titleOnlyQueues } = buildSearchUrlQueues(htmlEntries);\n        const apiIdentityCounts = countSearchIdentities(books.map((item) => ({\n            title: item.bookInfo?.title ?? '',\n            author: item.bookInfo?.author ?? '',\n        })));\n        const htmlIdentityCounts = countSearchIdentities(htmlEntries.filter((entry) => entry.author));\n        const apiTitleCounts = countSearchTitles(books.map((item) => ({ title: item.bookInfo?.title ?? '' })));\n        const htmlTitleCounts = countSearchTitles(htmlEntries);\n        return books.slice(0, Number(args.limit)).map((item, i) => {\n            const title = item.bookInfo?.title ?? '';\n            const author = item.bookInfo?.author ?? '';\n            return {\n                rank: i + 1,\n                title,","sourceCodeStart":134,"sourceCodeEnd":170,"githubUrl":"https://github.com/jackwener/OpenCLI/blob/49907e53dc3ade5c223ff0c4c2c2785687cec4e6/clis/weread/search.js#L134-L170","documentation":"Thrown in the `weread search` command when the public /search/global API response is fetched but its `books` field is not an array. The library uses `data?.books ?? []` then explicitly checks Array.isArray, so a payload where `books` is an object, string, or structurally unexpected shape surfaces this CommandExecutionError instead of crashing later on .map().","triggerScenarios":"fetchWebApi('/search/global', { keyword }) returns JSON but `data.books` is not an array — e.g. WeRead changed the response schema, returned {books: {...}} or an error envelope like {errCode, errMsg} with no books field, or returned an HTML/edge page that accidentally parsed as JSON.","commonSituations":"WeRead shipping an API contract change (relocating results to another field); a captive portal / anti-bot JSON error response; the keyword triggering a different error envelope from the API.","solutions":["Log the raw response (resp.json() output) from /search/global to see the actual shape WeRead now returns.","Check whether the response is an API error envelope (errCode/errmsg) and handle/surface that before indexing .books.","If WeRead renamed the field, update the extraction in search.js (e.g. data.books → data.result.books or similar) and pin/patch your CLI version.","File/track an upstream issue if it's a schema change; meanwhile rely on the HTML entries path or another search method.","Add a defensive parse helper that validates the payload shape before use so future changes produce clearer diagnostics."],"exampleFix":"// before\nconst books = data?.books ?? [];\nif (!Array.isArray(books)) throw new CommandExecutionError('...unreadable books payload');\n// after\nconst raw = data?.books ?? data?.result?.books ?? data?.data?.books;\nconst books = Array.isArray(raw) ? raw : (() => { console.error('unexpected payload:', JSON.stringify(data).slice(0, 500)); throw new CommandExecutionError('...unreadable books payload'); })();","handlingStrategy":"type-guard","validationCode":null,"typeGuard":"function hasBooksArray(data) {\n  return data != null && typeof data === 'object' && Array.isArray(data.books);\n}\n// usage: if (!hasBooksArray(data)) { /* handle schema drift before calling the command */ }","tryCatchPattern":"try {\n  const rows = await runWereadSearch(query);\n} catch (e) {\n  if (String(e.message).includes('unreadable books payload')) {\n    console.error('WeRead /search/global schema changed; inspect raw response and update parsing');\n    return fallbackToHtmlOnlyResults();\n  }\n  throw e;\n}","preventionTips":["Validate API payloads with a schema/type guard at the boundary before processing","Log raw /search/global responses periodically to detect schema drift early","Pin and test against a known-good CLI version; review WeRead API changelogs","Handle API error envelopes (errCode/errmsg) before reading data.books"],"tags":["schema","api-contract","weread","json","type-guard"],"backgroundTag":"unexpected-api-response-schema","analyzedSha":"49907e53dc3ade5c223ff0c4c2c2785687cec4e6","analyzedAt":"2026-08-29T08:14:47.543Z","schemaVersion":2},"datasetVersion":"2026-08-29T12:17:43.993Z"}