{"record":{"id":"fd09629e69607f5b","repo":"santifer/career-ops","slug":"glints-unexpected-api-response-json-stringify","errorCode":null,"errorMessage":"glints: unexpected API response — ${JSON.stringify(json).slice(0, 200)}","messagePattern":"glints: unexpected API response — (.+?)","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"providers/glints.mjs","lineNumber":229,"sourceCode":"          CountryCode: country,\n          includeExternalJobs: true,\n          pageSize: pageSize,\n          page: page,\n        },\n      };\n\n      let json;\n      try {\n        json = /** @type {any} */ (await graphqlPage(apiUrl, query, variables, ctx));\n      } catch (err) {\n        if (page === 1) throw err;\n        console.error(`glints: page ${page} fetch failed — ${err.message}`);\n        break;\n      }\n\n      const jobsInPage = json?.data?.searchJobsV3?.jobsInPage;\n      if (!Array.isArray(jobsInPage)) {\n        if (page === 1) throw new Error(`glints: unexpected API response — ${JSON.stringify(json).slice(0, 200)}`);\n        break;\n      }\n\n      if (jobsInPage.length === 0) break;\n\n      for (const item of jobsInPage) {\n        const job = parseGlintsItem(item, baseUrl, fallbackCompany);\n        if (job) allJobs.push(job);\n      }\n\n      // Stop if no more pages\n      if (json?.data?.searchJobsV3?.hasMore === false) break;\n      if (jobsInPage.length < pageSize) break;\n\n      // Rate-limit courtesy delay\n      await new Promise(resolve => setTimeout(resolve, 300));\n    }\n","sourceCodeStart":211,"sourceCodeEnd":247,"githubUrl":"https://github.com/santifer/career-ops/blob/aac998c7ed7248ea853b720ceeb1fdbeb322fc5d/providers/glints.mjs#L211-L247","documentation":"The Glints provider pages through a GraphQL searchJobsV3 endpoint and expects each response to contain data.searchJobsV3.jobsInPage as an array. When page 1 already fails to have that shape, the provider throws this error with a 200-char dump of the JSON to aid diagnosis. It guards against silently scanning zero jobs when the API contract changes or a proxy/CDN returns non-GraphQL payloads.","triggerScenarios":"First page of a fetch() call where json.data.searchJobsV3.jobsInPage is not an array — e.g. the API returns an errors array, an HTML error page parsed as JSON, a rate-limit JSON body, or Glints renames/versions the field (searchJobsV3 -> v4).","commonSituations":"Glints schema/GraphQL version change, blocked or rate-limited requests returning a challenge JSON, misconfigured query variables producing an errors payload, or an intermittent upstream outage on the very first request.","solutions":["Read the 200-char JSON snippet in the message to identify what the API actually returned (errors array? challenge page? empty object?).","Re-run the scan later — transient upstream errors often clear; the fetch loop itself breaks (not throws) for page > 1.","Update the extraction path in providers/glints.mjs if Glints changed the GraphQL schema (e.g. searchJobsV3 renamed).","Check for network middleboxes/proxies that intercept the HTTPS request and return their own JSON (captive portal, corporate proxy)."],"exampleFix":"// before\nconst jobsInPage = json?.data?.searchJobsV3?.jobsInPage;\n// after (tolerate a renamed/shifted payload)\nconst jobsInPage = json?.data?.searchJobsV3?.jobsInPage ?? json?.data?.searchJobs?.jobsInPage;","handlingStrategy":"type-guard","validationCode":"function looksLikeGlintsPayload(json) {\n  return Array.isArray(json?.data?.searchJobsV3?.jobsInPage);\n}\nif (!looksLikeGlintsPayload(await res.json())) throw new Error('glints: payload shape changed');","typeGuard":"const isGlintsPage = (v) => Array.isArray(v?.data?.searchJobsV3?.jobsInPage);","tryCatchPattern":"try {\n  await provider.fetch(entry, ctx);\n} catch (err) {\n  if (String(err.message).startsWith('glints: unexpected API response')) {\n    console.error(`glints schema drift for ${entry.name}:`, err.message);\n    return; // skip entry, keep scanning other boards\n  }\n  throw err;\n}","preventionTips":["Log the full upstream body (not just 200 chars) when the shape guard trips so schema drift is diagnosable.","Pin a small integration test that hits the Glints API weekly and asserts the jobsInPage path still exists.","Treat page>1 shape breaks as a break, page 1 as a throw — keep that asymmetry so partial data still flows.","Watch for GraphQL errors arrays in responses and surface them explicitly instead of as shape failures."],"tags":["api","graphql","unexpected-response","scraper"],"backgroundTag":"unexpected-api-response-shape","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"}