{"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":157,"sourceCode":"      for (let page = 1; page <= maxPages; page++) {\n        if (firstRequest) firstRequest = false;\n        else await sleep(INTER_PAGE_DELAY_MS);\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":139,"sourceCodeEnd":175,"githubUrl":"https://github.com/santifer/career-ops/blob/9b17a8ac97b398a496b38e423ae24e433b43254f/providers/alibaba.mjs#L139-L175","documentation":"The Alibaba careers provider POSTs to Alibaba's internal job-board API carrying a scraped CSRF token (XSRF-TOKEN cookie + x-xsrf-token header) and a body built from `buildBody(keyword, page)`. Alibaba reports failures in-band with HTTP 200 (a `success: false` body) rather than via status codes, so the provider checks `json?.success === false` and throws using `errorMsg || errorCode || 'success=false'`. This prevents a dead board from reading as an empty-but-alive one.","triggerScenarios":"After `ctx.fetchJson(...)` returns a JSON object whose `success` field is strictly `false`. The thrown message embeds `json.errorMsg`, falling back to `json.errorCode`, then the literal 'success=false'. Only fires before `succeededOnce` is set; a later failure is swallowed and the run returns the jobs collected so far.","commonSituations":"Expired or stale CSRF token (the token is derived/scraped upstream and can rotate), Alibaba board maintenance or rate-limiting returning a structured error, a malformed/unsupported keyword the API rejects, or an upstream API contract change adding a new failure shape.","solutions":["Read the embedded `errorMsg`/`errorCode` in the thrown message — it is the board's own failure reason and pinpoints the cause.","Verify the CSRF token acquisition step still produces a fresh, non-empty token before the POST loop.","Reproduce the POST manually (same cookie + x-xsrf-token header + body) to see the raw in-band error.","If the failure is intermittent, rely on the existing partial-result behaviour: the provider keeps jobs collected before the blip, so re-run after the board recovers.","If the API shape changed (no `success` field at all), update the success check in providers/alibaba.mjs:157 to match the new contract."],"exampleFix":"// before\nif (json?.success === false) {\n  throw new Error(`API error: ${json.errorMsg || json.errorCode || 'success=false'}`);\n}\n\n// after — also surface a non-200 status and log token presence for diagnostics\nif (json?.success === false) {\n  throw new Error(`API error: ${json.errorMsg || json.errorCode || 'success=false'} (csrf=${csrfToken ? 'present' : 'missing'})`);\n}","handlingStrategy":"retry","validationCode":"// Ensure the CSRF token is fresh and the keyword is non-empty before the POST loop\nif (!csrfToken || typeof csrfToken !== 'string') {\n  throw new Error('alibaba: missing CSRF token — cannot POST');\n}\nif (!keyword || typeof keyword !== 'string') {\n  throw new Error(`alibaba: invalid keyword for entry ${entry.name}`);\n}","typeGuard":"/** @param {any} j */\nfunction isAlibabaFailure(j) {\n  return j !== null && typeof j === 'object' && j.success === false;\n}","tryCatchPattern":"try {\n  const json = await ctx.fetchJson(url, opts);\n  if (json?.success === false) throw new Error(`API error: ${json.errorMsg || json.errorCode || 'success=false'}`);\n  succeededOnce = true;\n  // ...process jobs\n} catch (err) {\n  if (!succeededOnce) throw err;            // hard fail before any success\n  console.error(`⚠ alibaba: ${err.message} — keeping ${seen.size} jobs`);\n  return [...seen.values()];               // partial result on mid-run blip\n}","preventionTips":["Refresh the CSRF token immediately before the POST loop rather than caching it across runs.","Treat a `success:false` body as transient first (retry with backoff) before surfacing it as fatal.","Monitor the partial-result warning log to detect boards that are degrading but not fully dead."],"tags":["alibaba","api-error","csrf","scraping","in-band-error"],"backgroundTag":null,"analyzedSha":"9b17a8ac97b398a496b38e423ae24e433b43254f","analyzedAt":"2026-08-13T00:48:39.135Z","schemaVersion":2},"datasetVersion":"2026-08-13T04:17:16.726Z"}