santifer/career-ops · error · Error

wttj: /api/env payload has no JSON object

Error message

wttj: /api/env payload has no JSON object

What it means

parseEnvPayload extracts the window.env JSON object from the /api/env response text by slicing from the first '{' to the last '}'. If no braces exist (or they are misordered), it throws, meaning the fetched text does not contain any JSON object at all.

Solutions

  1. Inspect the fetched text (log it) to see what /api/env actually returned
  2. Retry the fetch — bot-protection pages are often transient
  3. Verify the /api/env endpoint URL is still correct for the WTTJ board
  4. If the payload format changed, update the extraction logic

Example fix

// before
const env = parseEnvPayload(await res.text());
// after
const text = await res.text();
if (!text.includes('{')) throw new Error(`/api/env returned non-JSON body: ${text.slice(0, 200)}`);
const env = parseEnvPayload(text);
Defensive patterns

Strategy: try-catch

Validate before calling

const text = await res.text();
if (typeof text !== 'string' || text.indexOf('{') === -1) throw new Error('env payload missing JSON object; got bot page or error page');

Type guard

const hasJsonObject = (text) => typeof text === 'string' && text.indexOf('{') !== -1 && text.lastIndexOf('}') > text.indexOf('{');

Try / catch

let env;
try { env = parseEnvPayload(text); } catch (e) { if (e.message.includes('no JSON object')) { console.warn('/api/env returned a non-JSON page (bot block or error page); retry later'); return null; } throw e; }

Prevention

When it happens

Trigger: parseEnvPayload called with an empty string, an HTML error/login page, or any text without a '{...}' region — i.e. /api/env did not return the expected env script payload.

Common situations: WTTJ serving a bot-block/Cloudflare challenge page instead of /api/env; network middleware returning an error page; the /api/env endpoint path changed or returned an empty body.

Related errors


AI-assisted analysis of santifer/career-ops@aac998c7ed (2026-09-16). Data as JSON: /api/errors/252433f5a7445d6c. Report an issue: GitHub.

Appendix: source

Thrown at providers/wttj.mjs:81

    throw new Error(`wttj: invalid URL: ${url}`);
  }
  if (parsed.protocol !== 'https:') throw new Error(`wttj: URL must use HTTPS: ${url}`);
  if (parsed.hostname !== host.toLowerCase()) {
    throw new Error(`wttj: untrusted ${label} hostname "${parsed.hostname}" — must be ${host}`);
  }
  return url;
}

/**
 * Parse the `window.env = {...}` payload served by /api/env and extract the
 * Algolia application id + client search key.
 * @param {string} text
 * @returns {{ appId: string, apiKey: string }}
 */
export function parseEnvPayload(text) {
  const start = text.indexOf('{');
  const end = text.lastIndexOf('}');
  if (start === -1 || end <= start) throw new Error('wttj: /api/env payload has no JSON object');
  let env;
  try {
    env = JSON.parse(text.slice(start, end + 1));
  } catch {
    throw new Error('wttj: /api/env payload is not valid JSON');
  }
  const appId = typeof env.PUBLIC_ALGOLIA_APPLICATION_ID === 'string' ? env.PUBLIC_ALGOLIA_APPLICATION_ID.trim() : '';
  const apiKey = typeof env.PUBLIC_ALGOLIA_API_KEY_CLIENT === 'string' ? env.PUBLIC_ALGOLIA_API_KEY_CLIENT.trim() : '';
  // App ids are short alphanumerics; validating keeps the derived Algolia
  // hostname from being attacker-shaped if the env payload ever changes.
  if (!/^[A-Z0-9]{6,16}$/i.test(appId)) throw new Error(`wttj: unexpected Algolia app id "${appId}"`);
  // The key is only ever sent as a request header (never used to build a
  // host), so don't over-constrain its format — WTTJ may rotate to a longer
  // or non-hex (e.g. secured/base64) client key. Length bounds only.
  if (!apiKey || apiKey.length < 16 || apiKey.length > 500) {
    throw new Error('wttj: unexpected Algolia api key shape');
  }
  return { appId, apiKey };

View on GitHub (pinned to aac998c7ed)