santifer/career-ops · error
greenhouse: invalid URL
Error message
greenhouse: invalid URL: ${url} What it means
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.
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.
Example fix
// before (portals.yml) api: boards-api.greenhouse.io/acme // after api: https://boards-api.greenhouse.io/acme
Defensive patterns
Strategy: validation
Validate before calling
function isValidGreenhouseUrl(url) {
try { new URL(url); return true; } catch { return false; }
}
if (!isValidGreenhouseUrl(entry.api)) throw new Error(`bad greenhouse api url: ${entry.api}`); Type guard
const isParseableUrl = (s) => { try { return Boolean(new URL(s)); } catch { return false; } }; Try / catch
try {
await provider.fetch(entry, ctx);
} catch (err) {
if (/greenhouse: invalid URL/.test(err.message)) {
console.error(`Fix portals.yml entry "${entry.name}": ${err.message}`);
return;
}
throw err;
} Prevention
- 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.
When it happens
Trigger: 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.
Common situations: 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.
Understand the failure class
Background: "Invalid URL" errors: why new URL(), URI.parse, and reqwest::Url reject your string — missing scheme, whitespace, and bad path format — this error's family across 39 libraries.
Related errors
- arbeitnow: invalid URL
- ashby: invalid URL
- bamboohr: invalid URL
- breezy: invalid URL
- builtin: invalid URL
AI-assisted analysis of santifer/career-ops@aac998c7ed (2026-09-16).
Data as JSON: /api/errors/ed0b2c96fa616552.
Report an issue: GitHub.
Appendix: source
Thrown at providers/greenhouse.mjs:26
// country_eligibility filter and visa_filter all read that field, and without
// it every Greenhouse board passed those filters blind.
import { htmlToText } from './_html-to-text.mjs';
const ALLOWED_GREENHOUSE_HOSTS = new Set([
'boards-api.greenhouse.io',
'boards.greenhouse.io',
'job-boards.greenhouse.io',
'job-boards.eu.greenhouse.io',
]);
/** @param {string} url */
function assertGreenhouseUrl(url) {
let parsed;
try {
parsed = new URL(url);
} catch {
throw new Error(`greenhouse: invalid URL: ${url}`);
}
if (parsed.protocol !== 'https:') throw new Error(`greenhouse: URL must use HTTPS: ${url}`);
if (!ALLOWED_GREENHOUSE_HOSTS.has(parsed.hostname))
throw new Error(`greenhouse: untrusted hostname "${parsed.hostname}" — must be one of: ${[...ALLOWED_GREENHOUSE_HOSTS].join(', ')}`);
return url;
}
/** @param {import('./_types.js').PortalEntry} entry */
function resolveApiUrl(entry) {
if (entry.api) {
assertGreenhouseUrl(entry.api);
return entry.api;
}
const url = entry.careers_url || '';
const match = url.match(/job-boards(?:\.eu)?\.greenhouse\.io\/([^/?#]+)/);
if (match) return `https://boards-api.greenhouse.io/v1/boards/${match[1]}/jobs`;
return null;
}View on GitHub (pinned to aac998c7ed)