santifer/career-ops · error · Error

breezy: cannot derive API URL for ${entry.name}

Error message

breezy: cannot derive API URL for ${entry.name}

What it means

Thrown by the breezy provider's fetch() when resolveOrigin(entry) returns null — i.e. the portal entry has no usable Breezy tenant origin. resolveOrigin honours an explicit entry.api, else parses entry.careers_url, and requires an https URL whose hostname matches /^[a-z0-9][a-z0-9-]*\.breezy\.hr$/ (a real per-tenant subdomain). The same condition makes detect() return null, so seeing this at runtime means fetch() was driven on an entry that never auto-detected as Breezy in the first place.

Source

Thrown at providers/breezy.mjs:66

    return null;
  }
  if (parsed.protocol !== 'https:') return null;
  if (!BREEZY_HOST_RE.test(parsed.hostname)) return null;
  return `https://${parsed.hostname}`;
}

/** @type {Provider} */
export default {
  id: 'breezy',

  detect(entry) {
    const origin = resolveOrigin(entry);
    return origin ? { url: `${origin}/json` } : null;
  },

  async fetch(entry, ctx) {
    const origin = resolveOrigin(entry);
    if (!origin) throw new Error(`breezy: cannot derive API URL for ${entry.name}`);
    const apiUrl = `${origin}/json`;
    assertBreezyUrl(apiUrl);
    // redirect:'error' + the host check above keep the final hostname pinned to
    // the tenant — a server-side redirect can't bounce us off-domain (SSRF).
    const json = /** @type {any} */ (await ctx.fetchJson(apiUrl, { redirect: 'error' }));
    return parseBreezyResponse(json, entry.name);
  },
};

/**
 * Parse a Breezy `<tenant>.breezy.hr/json` response. Exported for unit tests.
 *
 * Breezy returns a top-level array of positions:
 *   [{ name, url, published_date?,
 *      location: { name?, city?, state?, country?: { name }, is_remote? } }]
 *
 * - url: Breezy supplies an absolute posting URL on the tenant domain
 *   (`https://<tenant>.breezy.hr/p/<id>-<slug>`); it is the Job contract's dedup

View on GitHub (pinned to 9b17a8ac97)

Solutions

  1. Set careers_url (or api:) to the canonical Breezy board URL, e.g. https://acme.breezy.hr — the hostname must be <tenant>.breezy.hr over https.
  2. If the company fronts Breezy on a branded domain, find the underlying <tenant>.breezy.hr host (the /json feed only exists there) and put that in api:.
  3. Gate the entry with provider.detect(entry) before calling fetch() so misconfigured entries are skipped instead of throwing.

Example fix

# before (branded CNAME — will not resolve)
- name: Acme
  provider: breezy
  careers_url: https://careers.acme.com

# after (canonical tenant host)
- name: Acme
  provider: breezy
  careers_url: https://acme.breezy.hr
Defensive patterns

Strategy: validation

Validate before calling

// detect() runs the identical resolveOrigin check and returns null when fetch() would throw.
import breezy from './providers/breezy.mjs';
if (!breezy.detect(entry)) {
  // entry.api/careers_url is missing or not https://<tenant>.breezy.hr — skip or fix config
}

Type guard

/** True when entry can yield a Breezy tenant origin (fetch() will not throw 160). */
function isBreezyEntry(entry) {
  const raw = (typeof entry?.api === 'string' ? entry.api : '') || (typeof entry?.careers_url === 'string' ? entry.careers_url : '');
  if (!raw.trim()) return false;
  try {
    const u = new URL(raw);
    return u.protocol === 'https:' && /^[a-z0-9][a-z0-9-]*\.breezy\.hr$/.test(u.hostname);
  } catch { return false; }
}

Try / catch

// Prefer validation (above). If you must catch, match by provider prefix so you don't swallow unrelated errors.
try {
  await breezy.fetch(entry, ctx);
} catch (e) {
  if (String(e.message).startsWith('breezy: cannot derive API URL')) {
    // config issue — log entry.name and continue, do not retry
  } else { throw e; }
}

Prevention

When it happens

Trigger: entry.api and entry.careers_url are both empty/missing; or the configured URL is not https; or its hostname is not a literal <tenant>.breezy.hr subdomain (e.g. a branded CNAME like careers.company.com, or a bare 'breezy.hr' with no tenant).

Common situations: Pointing careers_url at the company's branded careers CNAME instead of the canonical <tenant>.breezy.hr host; copying an http:// URL from an older bookmark; omitting both api: and careers_url on a portals.yml entry whose provider is pinned to breezy.

Related errors


AI-assisted analysis of santifer/career-ops@9b17a8ac97 (2026-08-13). Data as JSON: /api/errors/90aa38445421ab17. Report an issue: GitHub.