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

  1. Open the portals.yml entry named in the error context and make the careers_url/api a full absolute URL: 'https://<tenant>.breezy.hr'.
  2. Trim the value and re-check for stray whitespace, BOM, or non-ASCII lookalike characters around the URL.
  3. Validate locally with `new URL(value)` in node before editing config to see the exact SyntaxError.
  4. 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

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


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)