jackwener/OpenCLI · error · CommandExecutionError

Failed to fetch WeRead search page: ${error instanceof Error

Error message

Failed to fetch WeRead search page: ${error instanceof Error ? error.message : String(error)}

What it means

Thrown by loadSearchHtmlEntries in clis/weread/search.js when the Node.js fetch() to the WeRead server-rendered search page (https://weread.qq.com/web/search/books?keyword=...) rejects before a response is available. The library wraps the raw fetch failure in a CommandExecutionError and embeds the underlying error message (e.g. DNS failure, ECONNREFUSED, TLS error, timeout). This is a network-level failure, not an HTTP error status.

Source

Thrown at clis/weread/search.js:110

    }
    return titleOnlyQueues.get(titleKey)?.shift() ?? '';
}
/**
 * Extract rendered search result reader URLs from the server-rendered search page.
 * The public JSON API still returns bookId, but the current web app links results
 * through /web/reader/<opaque-id> rather than /web/bookDetail/<bookId>.
 */
async function loadSearchHtmlEntries(query) {
    const url = new URL('/web/search/books', WEREAD_WEB_ORIGIN);
    url.searchParams.set('keyword', query);
    let resp;
    try {
        resp = await fetch(url.toString(), {
            headers: { 'User-Agent': WEREAD_UA },
        });
    }
    catch (error) {
        throw new CommandExecutionError(`Failed to fetch WeRead search page: ${error instanceof Error ? error.message : String(error)}`);
    }
    if (!resp.ok) {
        throw new CommandExecutionError(`WeRead search page request failed: HTTP ${resp.status}`);
    }
    const html = await resp.text();
    const items = Array.from(html.matchAll(/<li[^>]*class="wr_bookList_item"[^>]*>([\s\S]*?)<\/li>/g));
    return items.map((match) => {
        const chunk = match[1];
        const hrefMatch = chunk.match(/<a[^>]*href="([^"]+)"[^>]*class="wr_bookList_item_link"[^>]*>|<a[^>]*class="wr_bookList_item_link"[^>]*href="([^"]+)"[^>]*>/);
        const titleMatch = chunk.match(/<p[^>]*class="wr_bookList_item_title"[^>]*>([\s\S]*?)<\/p>/);
        const authorMatch = chunk.match(/<p[^>]*class="wr_bookList_item_author"[^>]*>([\s\S]*?)<\/p>/);
        const href = hrefMatch?.[1] || hrefMatch?.[2] || '';
        const title = decodeHtmlText(titleMatch?.[1] || '');
        const author = decodeHtmlText(authorMatch?.[1] || '');
        return {
            author,
            url: href ? new URL(href, WEREAD_WEB_ORIGIN).toString() : '',
            title,

View on GitHub (pinned to 49907e53dc)

Solutions

  1. Check basic connectivity: curl -I https://weread.qq.com/web/search/books — if this fails, fix network/DNS/proxy first.
  2. If behind a corporate proxy, configure the environment (HTTPS_PROXY) and use a fetch implementation that honors it (e.g. undici ProxyAgent) since Node's global fetch does not by default.
  3. Retry the command — transient network blips and connection resets are the most common cause.
  4. Check DNS: nslookup weread.qq.com; add the host to /etc/hosts if local DNS is unreliable.
  5. If the failure is persistent and pages are intermittently unreachable, consider adding retry/backoff around loadSearchHtmlEntries.

Example fix

// before
resp = await fetch(url.toString(), { headers: { 'User-Agent': WEREAD_UA } });
// after
const agent = process.env.HTTPS_PROXY ? new ProxyAgent(process.env.HTTPS_PROXY) : undefined;
resp = await fetch(url.toString(), { headers: { 'User-Agent': WEREAD_UA }, ...(agent ? { dispatcher: agent } : {}) });
Defensive patterns

Strategy: retry

Validate before calling

// Preflight before calling `weread search`
const ok = await fetch('https://weread.qq.com/web/search/books?keyword=ping', { method: 'HEAD' })
  .then(r => true).catch(() => false);
if (!ok) console.warn('weread.qq.com unreachable — check network/proxy/DNS before running search');

Try / catch

try {
  const results = await runWereadSearch(query);
} catch (e) {
  if (String(e.message).startsWith('Failed to fetch WeRead search page')) {
    // network-level: retry once, then surface connectivity guidance
    await sleep(2000);
    retryOrNotifyUser(e);
  } else throw e;
}

Prevention

When it happens

Trigger: fetch() rejects for https://weread.qq.com/web/search/books — DNS resolution failure, no network connectivity, TLS handshake failure, connection refused/reset, or an aborted/timeout request. Raised from the search command which calls loadSearchHtmlEntries in Promise.all alongside fetchWebApi('/search/global').

Common situations: Offline machine or flaky Wi-Fi; corporate proxy or firewall blocking weread.qq.com; DNS misconfiguration (especially for .qq.com domains outside China); IPv6 issues; system clock skew breaking TLS; Node without proxy env vars honored in an environment that requires one.

Related errors


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