{"record":{"id":"cfd66cbd0d27dc21","repo":"santifer/career-ops","slug":"builtin-invalid-url-url","errorCode":null,"errorMessage":"builtin: invalid URL: ${url}","messagePattern":"builtin: invalid URL: (.+?)","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"providers/builtin.mjs","lineNumber":169,"sourceCode":"  }\n  return HOSTS.get(h) ?? null;\n}\n\n/**\n * SSRF guard — every request URL passes through here before it is fetched. The\n * host comes from config, so this is the only thing standing between a\n * portals entry and an arbitrary fetch target. It checks the RESOLVED host\n * against the allowlist again rather than trusting the caller.\n *\n * @param {string} url\n * @returns {string}\n */\nfunction assertHost(url) {\n  let parsed;\n  try {\n    parsed = new URL(url);\n  } catch {\n    throw new Error(`builtin: invalid URL: ${url}`);\n  }\n  if (parsed.protocol !== 'https:') throw new Error(`builtin: URL must use HTTPS: ${url}`);\n  const host = parsed.hostname.toLowerCase();\n  if (HOSTS.get(host) !== host) {\n    throw new Error(`builtin: untrusted hostname \"${parsed.hostname}\" — must be one of ${[...new Set(HOSTS.values())].join(', ')}`);\n  }\n  return url;\n}\n\n/** @param {string} s */\nfunction stripTags(s) {\n  return decodeEntities(String(s).replace(/<[^>]*>/g, ' ')).replace(/\\s+/g, ' ').trim();\n}\n\n/**\n * Text of the first element following an icon marker inside a card.\n * Anchoring on the icon class (rather than on field order) is what keeps this\n * readable when Built In reshuffles the card layout.","sourceCodeStart":151,"sourceCodeEnd":187,"githubUrl":"https://github.com/santifer/career-ops/blob/aac998c7ed7248ea853b720ceeb1fdbeb322fc5d/providers/builtin.mjs#L151-L187","documentation":"The builtin (Built In) provider's assertHost SSRF guard throws this when the URL cannot be parsed by new URL(). All builtin provider request URLs pass through this guard, which re-checks the resolved host against the allowlist rather than trusting the caller. A malformed URL is rejected before any network request.","triggerScenarios":"A request URL built from a portals entry whose host string is so malformed that even resolveHost-style normalization fails downstream, or an internally composed URL with a bad scheme/path (e.g. missing origin, embedded spaces) reaching assertHost.","commonSituations":"Config value containing a full path or garbage that slips past resolveHost's tolerant parsing; interpolation bug producing 'https://undefined/jobs'; pasted URL with control characters or a typo like 'https:/builtinseattle.com' (single slash).","solutions":["Fix the host value in portals.yml to a bare allowlisted host such as 'www.builtinseattle.com' (no scheme, no path).","Test the string with `new URL('https://' + host + '/jobs')` in node to reproduce the parse error.","Trim invisible characters (BOM, non-breaking spaces) from the config value.","If composing URLs in code, validate the host with the exported resolveHost() helper before building the URL."],"exampleFix":"// before\nconst url = `https://${cfg.host}/jobs`; // cfg.host = 'https:/builtinseattle.com'\n// after\nconst host = resolveHost(cfg.host); // 'www.builtinseattle.com'\nif (!host) throw new Error('bad host config');\nconst url = `https://${host}/jobs`;","handlingStrategy":"validation","validationCode":"import { resolveHost } from './providers/builtin.mjs';\nconst host = resolveHost(cfg.host);\nif (host === null) throw new Error(`config: builtin host not allowlisted: ${cfg.host}`);","typeGuard":"function isPlainHost(v) { return typeof v === 'string' && v.trim() !== '' && !v.includes(' '); }","tryCatchPattern":"try {\n  await provider.fetch(entry, ctx);\n} catch (err) {\n  if (String(err.message).startsWith('builtin: invalid URL')) {\n    console.warn(`Skipping ${entry.name}: unparseable builtin host config`);\n    return null;\n  }\n  throw err;\n}","preventionTips":["Store bare hostnames (no scheme, no path) in the builtin host config field.","Validate every host with the exported resolveHost() helper before composing request URLs.","Strip whitespace and invisible characters from config values at load time.","Avoid interpolating possibly-undefined variables into URL templates."],"tags":["url","validation","ssrf-guard","config"],"backgroundTag":"invalid-url-format","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"}