santifer/career-ops · error
breezy: invalid URL
Error message
breezy: invalid URL: ${url} What it means
The Breezy HR provider's assertBreezyUrl guard throws this when the URL string cannot be parsed by the WHATWG URL constructor. Every request URL in the provider passes through this SSRF guard before fetching, so a malformed URL is rejected before any network I/O. It means the value (typically derived from a portals.yml entry's api or careers_url field) is not a syntactically valid absolute URL.
Solutions
- Open the portals.yml entry named in the error context and make the careers_url/api a full absolute URL: 'https://<tenant>.breezy.hr'.
- Trim the value and re-check for stray whitespace, BOM, or non-ASCII lookalike characters around the URL.
- Validate locally with `new URL(value)` in node before editing config to see the exact SyntaxError.
- If the value is built programmatically, ensure the origin part is non-empty before appending '/json'.
Example fix
// before careers_url: acme.breezy.hr // after careers_url: https://acme.breezy.hr
Defensive patterns
Strategy: validation
Validate before calling
function isValidBreezyUrl(url) {
try { new URL(url); return true; } catch { return false; }
}
if (!isValidBreezyUrl(entry.careers_url)) throw new Error(`config: not a valid URL: ${entry.careers_url}`); Type guard
function isNonEmptyString(v) { return typeof v === 'string' && v.trim().length > 0; } Try / catch
try {
await provider.fetch(entry, ctx);
} catch (err) {
if (String(err.message).startsWith('breezy: invalid URL')) {
console.warn(`Skipping ${entry.name}: malformed careers_url — fix portals.yml`);
return null;
}
throw err;
} Prevention
- Always store full absolute https:// URLs in portals.yml, never bare hosts.
- Run a config linter that calls new URL() on every careers_url/api field at load time.
- Trim and strip BOM/non-breaking spaces from config values before use.
- Keep URL templates out of string interpolation with possibly-undefined variables.
When it happens
Trigger: Calling provider fetch/detect with a portals entry whose careers_url or api string is malformed: missing scheme ('acme.breezy.hr/json'), misspelled scheme ('htps://...'), whitespace/control characters in the URL, or a bare path. new URL() throws and the catch block rethrows as this error.
Common situations: Hand-editing portals.yml and omitting 'https://'; pasting a URL with a leading/trailing invisible character or non-breaking space; templating mistakes where a variable interpolates empty or partial, yielding '/json' or 'https:///json'.
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
- builtin: invalid URL
- careerviet: invalid URL
- eightfold: invalid URL
- arbeitnow: invalid URL
- ashby: invalid URL
AI-assisted analysis of santifer/career-ops@aac998c7ed (2026-09-16).
Data as JSON: /api/errors/e572f9f608fc93af.
Report an issue: GitHub.
Appendix: source
Thrown at providers/breezy.mjs:24
// Per-tenant subdomains are the variable part, so SSRF defence uses a regex
// match on `<safe-tenant>.breezy.hr` rather than a static allowlist (same
// approach as the recruitee / bamboohr providers).
//
// Breezy boards expose every published position as a public JSON array at
// `<tenant>.breezy.hr/json` — title, absolute url, location, and a published
// date, all in the list payload at zero token cost (no per-job request, so the
// scanner stays zero-token). Breezy's authenticated REST API (api.breezy.hr) is
// intentionally NOT used; only the public board feed.
const BREEZY_HOST_RE = /^[a-z0-9][a-z0-9-]*\.breezy\.hr$/;
/** @param {string} url */
function assertBreezyUrl(url) {
let parsed;
try {
parsed = new URL(url);
} catch {
throw new Error(`breezy: invalid URL: ${url}`);
}
if (parsed.protocol !== 'https:') throw new Error(`breezy: URL must use HTTPS: ${url}`);
if (!BREEZY_HOST_RE.test(parsed.hostname)) {
throw new Error(`breezy: untrusted hostname "${parsed.hostname}" — must match <tenant>.breezy.hr`);
}
return url;
}
/**
* Resolve the tenant origin (`https://<tenant>.breezy.hr`) from an entry.
* Honours an explicit `api:` URL, else parses `careers_url`.
* @param {import('./_types.js').PortalEntry} entry
* @returns {string | null}
*/
function resolveOrigin(entry) {
const rawApi = typeof entry.api === 'string' ? entry.api : '';
const rawCareers = typeof entry.careers_url === 'string' ? entry.careers_url : '';
const raw = (rawApi || rawCareers).trim();View on GitHub (pinned to aac998c7ed)