santifer/career-ops · error · Error
bamboohr: cannot derive API URL for ${entry.name}
Error message
bamboohr: cannot derive API URL for ${entry.name} What it means
BambooHR's `resolveOrigin` returns null when neither `entry.api` nor `entry.careers_url` yields a `<tenant>.bamboohr.com` origin. `fetch` then throws, naming the entry. `detect()` returns null on the same condition (skipping the provider); this throw only fires if `fetch` is called directly.
Source
Thrown at providers/bamboohr.mjs:65
return null;
}
if (parsed.protocol !== 'https:') return null;
if (!BAMBOOHR_HOST_RE.test(parsed.hostname)) return null;
return `https://${parsed.hostname}`;
}
/** @type {Provider} */
export default {
id: 'bamboohr',
detect(entry) {
const origin = resolveOrigin(entry);
return origin ? { url: `${origin}/careers/list` } : null;
},
async fetch(entry, ctx) {
const origin = resolveOrigin(entry);
if (!origin) throw new Error(`bamboohr: cannot derive API URL for ${entry.name}`);
const apiUrl = `${origin}/careers/list`;
assertBambooHRUrl(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 parseBambooHRResponse(json, entry.name, origin);
},
};
/**
* Parse a BambooHR `/careers/list` response. Exported for unit tests.
*
* BambooHR returns:
* { meta: {...}, result: [{ id, jobOpeningName,
* location: { city?, state? }, isRemote?, employmentStatusLabel? }] }
*
* - url: built as `<origin>/careers/<id>` — matches the public
* `jobOpeningShareUrl`. Rows without a non-empty `id` are dropped (no stableView on GitHub (pinned to 9b17a8ac97)
Solutions
- Add `api:` or `careers_url:` pointing to `https://<tenant>.bamboohr.com`.
- Verify the company actually uses BambooHR before keeping the provider.
- Use `detect()` before `fetch` so unconfigured entries are skipped rather than throwing.
Example fix
# before - name: Acme provider: bamboohr careers_url: https://careers.acme.com/ # after - name: Acme provider: bamboohr careers_url: https://acme.bamboohr.com/
Defensive patterns
Strategy: type-guard
Validate before calling
// Use detect() before fetch; skip entries that don't resolve
const detected = provider.detect(entry);
if (!detected) {
console.warn(`bamboohr: skipping ${entry.name} — no <tenant>.bamboohr.com origin (add api/careers_url)`);
continue;
} Type guard
const RE = /^[a-z0-9][a-z0-9-]*\.bamboohr\.com$/;
/** @param {any} e */
function hasBambooOrigin(e) {
const u = e?.api ?? e?.careers_url;
if (typeof u !== 'string') return false;
try { return RE.test(new URL(u).hostname); } catch { return false; }
} Try / catch
try {
const origin = resolveOrigin(entry);
if (!origin) throw new Error(`bamboohr: cannot derive API URL for ${entry.name}`);
// ...fetch
} catch (err) {
if (/cannot derive API URL/.test(err.message)) { console.warn(err.message); return []; }
throw err;
} Prevention
- Always gate fetch with detect().
- Validate api/careers_url against the tenant regex at config load.
- Flag entries declaring bamboohr but lacking a bamboohr.com URL in a config report.
When it happens
Trigger: `resolveOrigin(entry)` returns null — the entry has no `api:` and no `careers_url` on a bamboohr.com host. Thrown at the top of `fetch` before any network call.
Common situations: An entry declares `provider: bamboohr` but only carries a corporate careers_url, the bamboohr fields were omitted, or a YAML indentation error orphaned them.
Related errors
- beesite: cannot resolve search host for ${entry.name}
- ashby: cannot derive API URL for ${entry.name}
- avature: cannot resolve origin for ${entry.name}
- bamboohr: invalid URL: ${url}
- local-parser: parser.command is required
AI-assisted analysis of santifer/career-ops@9b17a8ac97 (2026-08-13).
Data as JSON: /api/errors/a06a50c7ba0ead2e.
Report an issue: GitHub.