jackwener/OpenCLI · error · CommandExecutionError

WeRead book search returned malformed books

Error message

WeRead book search returned malformed books

What it means

searchBookByQuery calls the /web/search/global JSON endpoint and expects data.books to be an array; when the shape differs it throws CommandExecutionError 'WeRead book search returned malformed books'. This is a response-shape contract check: the HTTP call succeeded but the payload does not match the expected schema.

Source

Thrown at clis/weread/book-search.js:221

    const title = normalizeSearchText(book.title);
    const author = normalizeSearchText(book.author);
    if (!title)
        return '';
    if (author) {
        const exact = htmlEntries.filter((entry) => normalizeSearchText(entry.title) === title && normalizeSearchText(entry.author) === author);
        if (exact.length === 1)
            return exact[0].readerUrl;
    }
    const sameTitle = htmlEntries.filter((entry) => normalizeSearchText(entry.title) === title);
    return sameTitle.length === 1 ? sameTitle[0].readerUrl : '';
}

async function searchBookByQuery(bookQuery, bookRank) {
    const url = new URL('/web/search/global', `${WEREAD_WEB_ORIGIN}/web`);
    url.searchParams.set('keyword', bookQuery);
    const data = await fetchJson(url, 'WeRead book search');
    if (!Array.isArray(data?.books)) {
        throw new CommandExecutionError('WeRead book search returned malformed books');
    }
    const books = data.books;
    if (books.length === 0) {
        throw new EmptyResultError('weread book-search', `No WeRead books found for "${bookQuery}"`);
    }
    if (bookRank > books.length) {
        throw new ArgumentError(`book-rank must be <= ${books.length}`, `Only ${books.length} book search result(s) were returned for "${bookQuery}"`);
    }
    const bookInfo = books[bookRank - 1]?.bookInfo ?? {};
    const selected = {
        bookId: normalizeSearchText(bookInfo.bookId),
        title: normalizeSearchText(bookInfo.title),
        author: normalizeSearchText(bookInfo.author),
        readerUrl: '',
        chapters: [],
    };
    if (!selected.bookId) {
        throw new CommandExecutionError(`WeRead book search result ${bookRank} is missing bookId`);

View on GitHub (pinned to 49907e53dc)

Solutions

  1. Log the raw response body to see the actual shape WeRead returned
  2. Update the CLI parser if the API schema changed (books moved/renamed)
  3. Check whether the endpoint now requires authentication/cookies and supply them
  4. Verify no proxy is rewriting the response
  5. Report/patch the schema check in searchBookByQuery to match the new shape

Example fix

// before
const data = await fetchJson(url, 'WeRead book search');
if (!Array.isArray(data?.books)) {
  throw new CommandExecutionError('WeRead book search returned malformed books');
}
// after (accept alternative shapes and surface the real body)
const data = await fetchJson(url, 'WeRead book search');
const books = data?.books ?? data?.results?.books;
if (!Array.isArray(books)) {
  throw new CommandExecutionError('WeRead book search returned malformed books: ' + JSON.stringify(data).slice(0, 200));
}
Defensive patterns

Strategy: type-guard

Validate before calling

// Validate the API payload before handing it to the CLI's expectations
const data = await fetchJson(url, 'WeRead book search');
if (!isBooksPayload(data)) {
  console.error('Unexpected WeRead payload:', JSON.stringify(data).slice(0, 200));
}

Type guard

function isBooksPayload(v) {
  return v !== null && typeof v === 'object'
    && Array.isArray(v.books)
    && v.books.every((b) => b === null || typeof b === 'object');
}

Try / catch

try {
  await runCommand(['book-search', '--query', q]);
} catch (e) {
  if (e instanceof CommandExecutionError && e.message.includes('malformed books')) {
    console.error('WeRead API schema changed or auth is required. Dump the raw response and update the parser.');
  } else throw e;
}

Prevention

When it happens

Trigger: WeRead returns a valid 200 JSON body whose top-level structure changed (e.g. {results:...} instead of {books:[...]}), a login/captcha JSON object ({code:..., msg:...}), or null/undefined data from an unexpected payload.

Common situations: Silent API schema changes by WeRead, authenticated-only responses returned without an error status, regional API variants, or a reverse proxy substituting its own JSON error body.

Understand the failure class

Related errors


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