santifer/career-ops · error · Error

wttj: /api/env payload is not valid JSON

Error message

wttj: /api/env payload is not valid JSON

What it means

After slicing out the {...} region, parseEnvPayload runs JSON.parse on it; if that throws, the text between the outermost braces is not valid JSON. This means /api/env contained brace characters but not a parseable JSON object.

Solutions

  1. Log and inspect the sliced text to see why JSON.parse fails
  2. Retry the fetch to rule out a truncated response
  3. If WTTJ changed the env format, update parseEnvPayload's extraction and key names
  4. Confirm the expected keys (PUBLIC_ALGOLIA_APPLICATION_ID, PUBLIC_ALGOLIA_API_KEY_CLIENT) still exist after parsing

Example fix

// before
env = JSON.parse(text.slice(start, end + 1));
// after
try {
  env = JSON.parse(text.slice(start, end + 1));
} catch (e) {
  throw new Error(`wttj: /api/env payload is not valid JSON: ${e.message}; got: ${text.slice(start, Math.min(start + 200, end + 1))}`);
}
Defensive patterns

Strategy: try-catch

Validate before calling

let env;
try { env = JSON.parse(text.slice(text.indexOf('{'), text.lastIndexOf('}') + 1)); } catch { throw new Error('env payload not valid JSON'); }
if (typeof env.PUBLIC_ALGOLIA_APPLICATION_ID !== 'string') throw new Error('env payload missing app id');

Type guard

function isValidEnvPayload(env) {
  return env !== null && typeof env === 'object'
    && typeof env.PUBLIC_ALGOLIA_APPLICATION_ID === 'string'
    && typeof env.PUBLIC_ALGOLIA_API_KEY_CLIENT === 'string';
}

Try / catch

try { const { appId, apiKey } = parseEnvPayload(text); return { appId, apiKey }; } catch (e) { if (e.message.includes('not valid JSON')) { console.warn('/api/env JSON parse failed; payload format may have changed'); return null; } throw e; }

Prevention

When it happens

Trigger: parseEnvPayload given text like 'function() { return x }' or truncated/malformed JSON where the {...} slice fails JSON.parse.

Common situations: The env payload changed format (e.g. now a JS script instead of JSON); partial/truncated response due to network issues; an interstitial page containing braces in HTML/JS.

Understand the failure class

Background: JSON parse error: "Unexpected token" / "not valid JSON" / "failed to parse" — what JSON parsers are really complaining about — this error's family across 45 libraries.

Related errors


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

Appendix: source

Thrown at providers/wttj.mjs:86

  }
  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 };
}

/**
 * Normalize a single Algolia hit. Exported for tests.
 *

View on GitHub (pinned to aac998c7ed)