{"record":{"id":"252433f5a7445d6c","repo":"santifer/career-ops","slug":"wttj-api-env-payload-has-no-json-object","errorCode":null,"errorMessage":"wttj: /api/env payload has no JSON object","messagePattern":"wttj: /api/env payload has no JSON object","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"providers/wttj.mjs","lineNumber":56,"sourceCode":"    throw new Error(`wttj: invalid URL: ${url}`);\n  }\n  if (parsed.protocol !== 'https:') throw new Error(`wttj: URL must use HTTPS: ${url}`);\n  if (parsed.hostname !== host.toLowerCase()) {\n    throw new Error(`wttj: untrusted ${label} hostname \"${parsed.hostname}\" — must be ${host}`);\n  }\n  return url;\n}\n\n/**\n * Parse the `window.env = {...}` payload served by /api/env and extract the\n * Algolia application id + client search key.\n * @param {string} text\n * @returns {{ appId: string, apiKey: string }}\n */\nexport function parseEnvPayload(text) {\n  const start = text.indexOf('{');\n  const end = text.lastIndexOf('}');\n  if (start === -1 || end <= start) throw new Error('wttj: /api/env payload has no JSON object');\n  let env;\n  try {\n    env = JSON.parse(text.slice(start, end + 1));\n  } catch {\n    throw new Error('wttj: /api/env payload is not valid JSON');\n  }\n  const appId = typeof env.PUBLIC_ALGOLIA_APPLICATION_ID === 'string' ? env.PUBLIC_ALGOLIA_APPLICATION_ID.trim() : '';\n  const apiKey = typeof env.PUBLIC_ALGOLIA_API_KEY_CLIENT === 'string' ? env.PUBLIC_ALGOLIA_API_KEY_CLIENT.trim() : '';\n  // App ids are short alphanumerics; validating keeps the derived Algolia\n  // hostname from being attacker-shaped if the env payload ever changes.\n  if (!/^[A-Z0-9]{6,16}$/i.test(appId)) throw new Error(`wttj: unexpected Algolia app id \"${appId}\"`);\n  // The key is only ever sent as a request header (never used to build a\n  // host), so don't over-constrain its format — WTTJ may rotate to a longer\n  // or non-hex (e.g. secured/base64) client key. Length bounds only.\n  if (!apiKey || apiKey.length < 16 || apiKey.length > 500) {\n    throw new Error('wttj: unexpected Algolia api key shape');\n  }\n  return { appId, apiKey };","sourceCodeStart":38,"sourceCodeEnd":74,"githubUrl":"https://github.com/santifer/career-ops/blob/9b17a8ac97b398a496b38e423ae24e433b43254f/providers/wttj.mjs#L38-L74","documentation":"parseEnvPayload extracts the substring between the first { and the last } of the /api/env response (the window.env = {...} block). This throws when there is no { at all, or no } after it — i.e. the response contains no JSON-object-looking span.","triggerScenarios":"/api/env returned an HTML error page, an empty body, plain text, or a payload whose braces are in the wrong order. Most often a transient error page from WTTJ/CDN, or a permanent markup change at WTTJ (no more literal window.env = {...}).","commonSituations":"WTTJ is returning a 200 HTML error/maintenance page; a CDN interstitial; WTTJ refactored the env bootstrap so it is no longer a literal window.env assignment.","solutions":["Fetch https://www.welcometothejungle.com/api/env in a browser and confirm it still contains a window.env = {...} block.","If it is an error page, retry later.","If WTTJ changed the bootstrap, update parseEnvPayload's extraction to match (scope to the window.env assignment).","Check the HTTP status — a non-200 may be masked by a 200 HTML page."],"exampleFix":"// before — relies on literal window.env braces anywhere in the page\nconst start = text.indexOf(\"{\");\nconst end = text.lastIndexOf(\"}\");\nif (start === -1 || end <= start) throw new Error(\"wttj: /api/env payload has no JSON object\");\n// after — find the window.env assignment explicitly\nconst m = text.match(/window\\.env\\s*=\\s*(\\{[\\s\\S]*\\})\\s*;?\\s*<\\/?script/i);\nif (!m) throw new Error(\"wttj: /api/env payload has no JSON object\");\nlet env;\ntry { env = JSON.parse(m[1]); }\ncatch { throw new Error(\"wttj: /api/env payload is not valid JSON\"); }","handlingStrategy":"try-catch","validationCode":"const text = await fetch(ENV_URL).then(r => r.text());\nif (text.indexOf(\"{\") === -1 || text.lastIndexOf(\"}\") <= text.indexOf(\"{\")) {\n  console.warn(\"wttj /api/env has no JSON object — possible maintenance page\");\n}","typeGuard":"const hasJsonObject = (t) => typeof t === \"string\" &&\n  t.indexOf(\"{\") !== -1 && t.lastIndexOf(\"}\") > t.indexOf(\"{\");","tryCatchPattern":"let creds;\ntry {\n  creds = parseEnvPayload(envText);\n} catch (err) {\n  if (/\\/api\\/env payload/.test(err.message)) { logUpstreamChange(\"wttj\", err.message); }\n  throw err;\n}","preventionTips":["Smoke-test /api/env in CI to catch a markup change early.","Alert on this error — it almost always means WTTJ changed something.","Distinguish a transient error page from a permanent markup change by retrying once."],"tags":["api-contract","wttj","parsing","algolia"],"backgroundTag":null,"analyzedSha":"9b17a8ac97b398a496b38e423ae24e433b43254f","analyzedAt":"2026-08-13T00:48:39.135Z","schemaVersion":2},"datasetVersion":"2026-08-13T04:17:16.726Z"}