{"record":{"id":"ed0b2c96fa616552","repo":"santifer/career-ops","slug":"greenhouse-invalid-url-url","errorCode":null,"errorMessage":"greenhouse: invalid URL: ${url}","messagePattern":"greenhouse: invalid URL: (.+?)","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"providers/greenhouse.mjs","lineNumber":26,"sourceCode":"// country_eligibility filter and visa_filter all read that field, and without\n// it every Greenhouse board passed those filters blind.\n\nimport { htmlToText } from './_html-to-text.mjs';\n\nconst ALLOWED_GREENHOUSE_HOSTS = new Set([\n  'boards-api.greenhouse.io',\n  'boards.greenhouse.io',\n  'job-boards.greenhouse.io',\n  'job-boards.eu.greenhouse.io',\n]);\n\n/** @param {string} url */\nfunction assertGreenhouseUrl(url) {\n  let parsed;\n  try {\n    parsed = new URL(url);\n  } catch {\n    throw new Error(`greenhouse: invalid URL: ${url}`);\n  }\n  if (parsed.protocol !== 'https:') throw new Error(`greenhouse: URL must use HTTPS: ${url}`);\n  if (!ALLOWED_GREENHOUSE_HOSTS.has(parsed.hostname))\n    throw new Error(`greenhouse: untrusted hostname \"${parsed.hostname}\" — must be one of: ${[...ALLOWED_GREENHOUSE_HOSTS].join(', ')}`);\n  return url;\n}\n\n/** @param {import('./_types.js').PortalEntry} entry */\nfunction resolveApiUrl(entry) {\n  if (entry.api) {\n    assertGreenhouseUrl(entry.api);\n    return entry.api;\n  }\n  const url = entry.careers_url || '';\n  const match = url.match(/job-boards(?:\\.eu)?\\.greenhouse\\.io\\/([^/?#]+)/);\n  if (match) return `https://boards-api.greenhouse.io/v1/boards/${match[1]}/jobs`;\n  return null;\n}","sourceCodeStart":8,"sourceCodeEnd":44,"githubUrl":"https://github.com/santifer/career-ops/blob/aac998c7ed7248ea853b720ceeb1fdbeb322fc5d/providers/greenhouse.mjs#L8-L44","documentation":"assertGreenhouseUrl validates every Greenhouse URL before any network request is made. If the string cannot be parsed by the URL constructor (empty, malformed, missing scheme, illegal characters), the provider throws this error immediately. It is a fail-fast input validation so the scan never hits the network with a broken URL.","triggerScenarios":"Calling assertGreenhouseUrl (via resolveApiUrl or fetch) with a value that new URL() rejects: an empty string, a board token with spaces, a careers_url like 'greenhouse.io/acme' without a scheme, or a truncated URL from a config typo.","commonSituations":"portals.yml entries with a bad api:/careers_url value, hand-edited config missing 'https://', environment-variable interpolation producing an empty string, or copy-paste that dropped part of the URL.","solutions":["Print the offending URL from the error message and fix the entry in portals.yml (usually add the https:// scheme).","Validate config at startup with new URL() on every api:/careers_url field before scanning.","Check for shell/env expansion issues that left the variable empty.","If the URL is built programmatically, ensure all parts are joined with proper separators and no whitespace."],"exampleFix":"// before (portals.yml)\napi: boards-api.greenhouse.io/acme\n// after\napi: https://boards-api.greenhouse.io/acme","handlingStrategy":"validation","validationCode":"function isValidGreenhouseUrl(url) {\n  try { new URL(url); return true; } catch { return false; }\n}\nif (!isValidGreenhouseUrl(entry.api)) throw new Error(`bad greenhouse api url: ${entry.api}`);","typeGuard":"const isParseableUrl = (s) => { try { return Boolean(new URL(s)); } catch { return false; } };","tryCatchPattern":"try {\n  await provider.fetch(entry, ctx);\n} catch (err) {\n  if (/greenhouse: invalid URL/.test(err.message)) {\n    console.error(`Fix portals.yml entry \"${entry.name}\": ${err.message}`);\n    return;\n  }\n  throw err;\n}","preventionTips":["Always write absolute https:// URLs in portals.yml entries.","Validate every api:/careers_url with new URL() at config-load time.","Trim whitespace from config values before use.","Add a schema/lint step for portals.yml in CI."],"tags":["url","validation","config","greenhouse"],"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"}