{"record":{"id":"3abad676a8fc1770","repo":"santifer/career-ops","slug":"api-error-json-errormsg-json-errorcode-s","errorCode":null,"errorMessage":"API error: ${json.errorMsg || json.errorCode || 'success=false'}","messagePattern":"API error: (.+?)","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"providers/alibaba.mjs","lineNumber":162,"sourceCode":"      for (let page = 1; page <= maxPages; page++) {\n        if (firstRequest) firstRequest = false;\n        else await sleep(INTER_PAGE_DELAY_MS, ctx);\n        let json;\n        try {\n          json = /** @type {any} */ (await ctx.fetchJson(API, {\n            method: 'POST',\n            headers: {\n              'content-type': 'application/json',\n              'cookie': `XSRF-TOKEN=${csrfToken}`,\n              'x-xsrf-token': csrfToken,\n            },\n            body: buildBody(keyword, page),\n            redirect: 'error',\n          }));\n          // The API reports failures in-band with HTTP 200; surface them so a\n          // dead board doesn't read as an empty-but-alive one.\n          if (json?.success === false) {\n            throw new Error(`API error: ${json.errorMsg || json.errorCode || 'success=false'}`);\n          }\n        } catch (err) {\n          // A dead board should still read as a failure, but a mid-run blip\n          // must not discard what's already collected (same idiom as\n          // workday/jobstreet/glints). Track successes directly — a keyword\n          // can legitimately match 0 jobs, so seen.size is not the signal.\n          if (!succeededOnce) throw err;\n          console.error(`  ⚠ alibaba: keyword \"${keyword}\" page ${page} failed (${err.message}) — keeping the ${seen.size} jobs collected so far`);\n          return [...seen.values()];\n        }\n        succeededOnce = true;\n        const { jobs, total } = parseAlibabaResponse(json, entry.name || '阿里巴巴');\n        if (jobs.length === 0) break;\n\n        for (const job of jobs) {\n          if (!seen.has(job.url)) seen.set(job.url, job);\n        }\n","sourceCodeStart":144,"sourceCodeEnd":180,"githubUrl":"https://github.com/santifer/career-ops/blob/aac998c7ed7248ea853b720ceeb1fdbeb322fc5d/providers/alibaba.mjs#L144-L180","documentation":"Alibaba's job API reports failures in-band: it returns HTTP 200 with { success: false, errorMsg/errorCode }. Because the HTTP status looks fine, the provider explicitly checks success===false and throws so a dead board isn't mistaken for an empty-but-alive one. The thrown message carries the API's own errorMsg or errorCode.","triggerScenarios":"Any keyword/page request where the API responds 200 with success:false — e.g. invalid or missing request parameters in buildBody, rate limiting expressed in-band, an internal API error, or a deprecated endpoint still answering 200 with an error envelope.","commonSituations":"Alibaba changed body parameters (keyword/page field names) so requests are rejected; IP-level throttling during a large sweep; the board was migrated or decommissioned; transient upstream errors mid-run (the catch block deliberately keeps already-collected jobs for blips).","solutions":["Read the errorMsg/errorCode in the message: a parameter error means buildBody's payload no longer matches the API — update keyword/page fields.","If it's a throttle code, slow the request pacing or reduce keywords per run and retry.","Retry the run if transient — mid-run blips don't discard collected jobs, but a persistent error on every request means the board/API changed.","Verify the endpoint is still live with a manual POST replicating buildBody's body; if the board is dead, remove or disable the portals.yml entry."],"exampleFix":"// before: treats any failure as fatal for that keyword\nthrow new Error(`API error: ${json.errorMsg || json.errorCode || 'success=false'}`);\n// after (caller side): tolerate transient in-band errors, keep prior pages\ntry { await fetchKeywordPage(page); } catch (err) {\n  if (seen.size > 0 && isTransient(err)) return; // keep collected jobs\n  throw err;\n}","handlingStrategy":"try-catch","validationCode":"// probe the board before a full sweep\nconst json = await fetch(ALIBABA_ENDPOINT, { method: 'POST', body: probeBody }).then(r => r.json());\nif (json?.success === false) throw new Error(`alibaba board unhealthy: ${json.errorMsg || json.errorCode}`);","typeGuard":"function isAlibabaError(json) {\n  return !!json && typeof json === 'object' && json.success === false;\n}","tryCatchPattern":"try {\n  await fetchKeyword(kw, page);\n} catch (err) {\n  if (String(err.message).startsWith('API error:') && collected > 0) {\n    console.warn(`alibaba: keyword \"${kw}\" failed mid-run, keeping ${collected} jobs: ${err.message}`);\n    return; // don't discard already-collected pages\n  }\n  throw err;\n}","preventionTips":["Never assume HTTP 200 means success with this API — always check the success flag in the body.","Pace requests to avoid in-band throttling errors during batch sweeps.","Log errorMsg/errorCode verbatim; it names the exact parameter or limit that failed.","Re-validate buildBody's field names whenever the provider hasn't run for a while."],"tags":["api","http-200-error","upstream","provider"],"backgroundTag":"api-error-response","analyzedSha":"aac998c7ed7248ea853b720ceeb1fdbeb322fc5d","analyzedAt":"2026-09-16T06:35:29.214Z","contentChangedAt":"2026-09-16T06:35:29.214Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}