{"record":{"id":"c606a60636036aca","repo":"jackwener/OpenCLI","slug":"fetch-error-c606a6","errorCode":"FETCH_ERROR","errorMessage":"FETCH_ERROR: HTTP ${resp.status} for ${path}","messagePattern":"FETCH_ERROR: HTTP (.+?) for (.+?)","errorType":"error_code","errorClass":"CliError","httpStatus":null,"severity":"error","filePath":"clis/weread/utils.js","lineNumber":114,"sourceCode":"      poll();\n    }))\n  `;\n}\n/**\n * Fetch a public WeRead web endpoint (Node.js direct fetch).\n * Used by search and ranking commands (browser: false).\n */\nexport async function fetchWebApi(path, params) {\n    const url = new URL(`${WEB_API}${path}`);\n    if (params) {\n        for (const [k, v] of Object.entries(params))\n            url.searchParams.set(k, v);\n    }\n    const resp = await fetch(url.toString(), {\n        headers: { 'User-Agent': WEREAD_UA },\n    });\n    if (!resp.ok) {\n        throw new CliError('FETCH_ERROR', `HTTP ${resp.status} for ${path}`, 'WeRead API may be temporarily unavailable');\n    }\n    try {\n        return await resp.json();\n    }\n    catch {\n        throw new CliError('PARSE_ERROR', `Invalid JSON response for ${path}`, 'WeRead may have returned an HTML error page');\n    }\n}\n/**\n * Fetch a private WeRead API endpoint with cookies extracted from the browser.\n * The HTTP request itself runs in Node.js to avoid page-context CORS failures.\n *\n * Cookies are collected from both the API subdomain (i.weread.qq.com) and the\n * main domain (weread.qq.com). WeRead may set auth cookies as host-only on\n * weread.qq.com, which won't match i.weread.qq.com in a URL-based lookup.\n */\nexport async function fetchPrivateApi(page, path, params) {\n    const url = new URL(`${API}${path}`);","sourceCodeStart":96,"sourceCodeEnd":132,"githubUrl":"https://github.com/jackwener/OpenCLI/blob/49907e53dc3ade5c223ff0c4c2c2785687cec4e6/clis/weread/utils.js#L96-L132","documentation":"Thrown by fetchWebApi in clis/weread/utils.js when a public WeRead web API call (https://weread.qq.com/web/*) returns a non-2xx HTTP status. It is a CliError with code FETCH_ERROR and a hint that the API may be temporarily unavailable. This covers all public endpoints used by browser-free commands like search and ranking.","triggerScenarios":"Any fetchWebApi call (e.g. /search/global with {keyword}) where resp.ok is false: 403/429 WAF or rate-limit responses, 404 after WeRead route changes, 5xx server errors, 30x followed to an error page.","commonSituations":"Rate limiting after scripted bulk queries; WeRead deploying new API paths; edge/CDN errors (502/504 from the qq.com CDN); anti-bot challenges returning 403 to non-browser fetches.","solutions":["Retry with exponential backoff — transient 5xx/429 is the most common cause.","Check the status: 403/429 → slow request rate, add browser-like headers, or supply cookies from a logged-in session; 404 → update the endpoint path after a WeRead API change; 5xx → wait for server recovery.","Verify the endpoint manually: curl -A '<UA>' 'https://weread.qq.com/web/search/global?keyword=x' and inspect the body.","Catch CliError with code 'FETCH_ERROR' in scripts and fall back to the HTML scraping path (loadSearchHtmlEntries).","Check weread.qq.com status in a browser to rule out a full outage."],"exampleFix":"// before\nconst data = await fetchWebApi('/search/global', { keyword: query });\n// after\nlet data;\ntry {\n  data = await fetchWebApi('/search/global', { keyword: query });\n} catch (e) {\n  if (e.code === 'FETCH_ERROR' && /HTTP (429|5\\d\\d)/.test(e.message)) {\n    await new Promise(r => setTimeout(r, 2000));\n    data = await fetchWebApi('/search/global', { keyword: query });\n  } else throw e;\n}","handlingStrategy":"retry","validationCode":"// Health probe of the public API before batch usage\nconst r = await fetch('https://weread.qq.com/web/search/global?keyword=ping', { headers: { 'User-Agent': WEREAD_UA } });\nif (!r.ok) console.warn(`weread web API unhealthy (HTTP ${r.status})`);","typeGuard":null,"tryCatchPattern":"try {\n  const data = await callWereadCommand();\n} catch (e) {\n  if (e.code === 'FETCH_ERROR' && /HTTP \\d{3}/.test(e.message)) {\n    const status = Number(/HTTP (\\d{3})/.exec(e.message)[1]);\n    if (status === 429 || status >= 500) return withBackoffRetry(callWereadCommand);\n    throw e;\n  }\n  throw e;\n}","preventionTips":["Add exponential backoff retries for 429/5xx statuses","Rate-limit scripted calls to /web/* endpoints","Catch on CliError.code rather than message text for stable handling","Re-verify endpoint paths after WeRead web deploys (404s indicate route changes)"],"tags":["http","weread","api","rate-limit","http-status"],"backgroundTag":"http-non-2xx-response","analyzedSha":"49907e53dc3ade5c223ff0c4c2c2785687cec4e6","analyzedAt":"2026-08-29T08:14:47.543Z","schemaVersion":2},"datasetVersion":"2026-08-29T12:17:43.993Z"}