{"record":{"id":"d92c596ba9b88dae","repo":"santifer/career-ops","slug":"gem-invalid-url-url","errorCode":null,"errorMessage":"gem: invalid URL: ${url}","messagePattern":"gem: invalid URL: (.+?)","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"providers/gem.mjs","lineNumber":111,"sourceCode":"// to before this field list was widened.\n/** @param {any} posting */\nfunction buildJobDescriptionText(posting) {\n  const intro = htmlToText(posting?.jobPostSectionHtml?.introHtml);\n  const body = htmlToText(posting?.descriptionHtml);\n  const outro = htmlToText(posting?.jobPostSectionHtml?.outroHtml);\n  const compensation = htmlToText(posting?.compensationHtml);\n\n  const text = [intro, body, outro].filter(Boolean).join('\\n\\n');\n  return compensation ? [text, `Compensation: ${compensation}`].filter(Boolean).join('\\n\\n') : text;\n}\n\n/** @param {string} url */\nfunction assertGemUrl(url) {\n  let parsed;\n  try {\n    parsed = new URL(url);\n  } catch {\n    throw new Error(`gem: invalid URL: ${url}`);\n  }\n  if (parsed.protocol !== 'https:') throw new Error(`gem: URL must use HTTPS: ${url}`);\n  if (!ALLOWED_GEM_HOSTS.has(parsed.hostname))\n    throw new Error(`gem: untrusted hostname \"${parsed.hostname}\" — must be one of: ${[...ALLOWED_GEM_HOSTS].join(', ')}`);\n  return url;\n}\n\n/** @param {import('./_types.js').PortalEntry} entry */\nfunction resolveBoardId(entry) {\n  const raw = typeof entry.careers_url === 'string' ? entry.careers_url : '';\n  if (!raw) return null;\n  let parsed;\n  try {\n    parsed = new URL(raw);\n  } catch {\n    return null;\n  }\n  if (parsed.hostname !== 'jobs.gem.com') return null;","sourceCodeStart":93,"sourceCodeEnd":129,"githubUrl":"https://github.com/santifer/career-ops/blob/9b17a8ac97b398a496b38e423ae24e433b43254f/providers/gem.mjs#L93-L129","documentation":"gem.mjs throws this inside assertGemUrl() when `new URL(url)` raises — i.e. the string is not an absolute, parseable URL. The guard runs before every POST to the Gem GraphQL batch endpoint. In production the validated value is the module constant GEM_API_URL ('https://jobs.gem.com/api/public/graphql/batch'), so a throw here means that constant was corrupted/injected or a test called assertGemUrl() with a relative/malformed string.","triggerScenarios":"GEM_API_URL was edited to a relative path or a string with illegal characters (spaces, stray quotes); an env var or config merge injected an empty/undefined-coerced string; a test invokes assertGemUrl('/api/graphql/batch') or assertGemUrl(undefined).","commonSituations":"A contributor changes the endpoint to a path-only string ('/api/public/graphql/batch') thinking it will be resolved against a base; a templating/CI step mangled the constant; a unit test passes a deliberately bad URL but the test expected a different message.","solutions":["Restore GEM_API_URL to the full absolute form 'https://jobs.gem.com/api/public/graphql/batch'.","If you meant to test the guard, pass an absolute-but-invalid string (e.g. 'https://') only when you intend to assert the throw.","Audit any code that builds/mutates GEM_API_URL at runtime — it is meant to be a literal constant."],"exampleFix":"// before\nconst GEM_API_URL = '/api/public/graphql/batch'; // relative -> new URL() throws\n\n// after\nconst GEM_API_URL = 'https://jobs.gem.com/api/public/graphql/batch';","handlingStrategy":"validation","validationCode":"// Startup self-check: GEM_API_URL must be an absolute URL.\nfunction checkGemApiUrl() {\n  try { new URL('https://jobs.gem.com/api/public/graphql/batch'); }\n  catch { throw new Error('GEM_API_URL is not a valid absolute URL'); }\n}","typeGuard":null,"tryCatchPattern":"// Defensive: validate before the first request so the error surfaces with context.\ntry { assertGemUrl(GEM_API_URL); }\ncatch (err) { throw new Error(`gem endpoint misconfigured: ${err.message}`); }","preventionTips":["Keep GEM_API_URL as a string literal; never build it from concatenation of env vars.","Add a CI unit test asserting assertGemUrl(GEM_API_URL) does not throw.","Review any PR that touches the GEM_API_URL line."],"tags":["url-validation","gem","config","ssrf-guard"],"backgroundTag":null,"analyzedSha":"9b17a8ac97b398a496b38e423ae24e433b43254f","analyzedAt":"2026-08-13T00:48:39.135Z","schemaVersion":2},"datasetVersion":"2026-08-13T04:17:16.726Z"}