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 dedupView on GitHub (pinned to 9b17a8ac97)
Solutions
- 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.
- 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:.
- 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
- Run provider.detect(entry) on every entry before fetch() and skip non-detecting ones.
- Validate portals.yml entries at startup against each provider's host pattern.
- Keep branded CNAMEs out of breezy entries — only <tenant>.breezy.hr resolves.
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
- comeet: cannot derive API URL for ${entry.name} (set api: to
- consider: ${entry.name} needs an https careers_url on a publ
- csod: cannot resolve careersite URL for ${entry.name}
- eightfold: cannot derive API URL for ${entry.name}
- breezy: invalid URL: ${url}
AI-assisted analysis of santifer/career-ops@9b17a8ac97 (2026-08-13).
Data as JSON: /api/errors/90aa38445421ab17.
Report an issue: GitHub.