santifer/career-ops · error
bamboohr: invalid URL
Error message
bamboohr: invalid URL: ${url} What it means
The BambooHR provider's assertBambooHRUrl first checks that the string parses as a URL via `new URL()`; unparseable input throws this error. It mirrors the Ashby gate: parseability, then HTTPS, then hostname pattern, in that order.
Solutions
- Inspect the exact URL value; ensure it is a full absolute URL including the scheme.
- Use `https://<tenant>.bamboohr.com` as the origin form in config, not a bare tenant name.
- Trim whitespace and expand any template placeholders in the config entry.
- Build origins with `new URL('https://' + tenant + '.bamboohr.com')` and validate the tenant name first (no slashes/spaces).
Example fix
// before
const origin = 'mycompany.bamboohr.com';
assertBambooHRUrl(`${origin}/careers/list`); // throws: not parseable
// after
const origin = 'https://mycompany.bamboohr.com';
assertBambooHRUrl(`${origin}/careers/list`); // ok Defensive patterns
Strategy: validation
Validate before calling
function isValidBambooUrl(url) {
if (typeof url !== 'string' || !url.trim()) return false;
try { new URL(url.trim()); return true; } catch { return false; }
} Type guard
function parseUrlSafe(value) {
try { return { ok: true, url: new URL(value) }; } catch { return { ok: false }; }
} Try / catch
try {
assertBambooHRUrl(apiUrl);
} catch (err) {
console.warn(`Skipping BambooHR entry: ${err.message}`);
return null;
} Prevention
- Store full `https://<tenant>.bamboohr.com` origins, never bare tenant names.
- Expand placeholders like `<tenant>` before running the scanner.
- Trim config values and validate them at load with `new URL()`.
- Validate tenant names are simple subdomain labels (letters/digits/hyphens).
When it happens
Trigger: Calling assertBambooHRUrl (or the fetch path that builds `<origin>/careers/list` and validates it) with a string `new URL()` rejects — empty string, missing scheme like `mycompany.bamboohr.com`, whitespace, or a malformed origin built from a bad tenant name.
Common situations: portals.yml BambooHR entries with just a tenant subdomain instead of the full URL, placeholders like `<tenant>.bamboohr.com` left literally in config, or an empty tenant value producing an empty/invalid origin string.
Understand the failure class
Background: "Invalid URL" / "URL cannot be empty": fix the malformed or missing URL behind request-construction failures — this error's family across 50 libraries.
Related errors
- arbeitnow: invalid URL
- ashby: invalid URL
- breezy: invalid URL
- builtin: invalid URL
- careerviet: invalid URL
AI-assisted analysis of santifer/career-ops@aac998c7ed (2026-09-16).
Data as JSON: /api/errors/686fd092a5ba9a5d.
Report an issue: GitHub.
Appendix: source
Thrown at providers/bamboohr.mjs:25
// Auto-detects from careers_url pattern `https://<tenant>.bamboohr.com[/...]`.
// Per-tenant subdomains are the variable part, so SSRF defence uses a regex
// match on `<safe-tenant>.bamboohr.com` rather than a static allowlist
// (same approach as the recruitee provider).
//
// The list endpoint (`/careers/list`) returns lightweight metadata — enough for
// the Job contract (title, url, location) at zero token cost. The full JD lives
// behind a second `/careers/<id>/detail` request, which the scanner deliberately
// skips to stay zero-token (so `description`/`postedAt` are omitted).
const BAMBOOHR_HOST_RE = /^[a-z0-9][a-z0-9-]*\.bamboohr\.com$/;
/** @param {string} url */
function assertBambooHRUrl(url) {
let parsed;
try {
parsed = new URL(url);
} catch {
throw new Error(`bamboohr: invalid URL: ${url}`);
}
if (parsed.protocol !== 'https:') throw new Error(`bamboohr: URL must use HTTPS: ${url}`);
if (!BAMBOOHR_HOST_RE.test(parsed.hostname)) {
throw new Error(`bamboohr: untrusted hostname "${parsed.hostname}" — must match <tenant>.bamboohr.com`);
}
return url;
}
/**
* Resolve the tenant origin (`https://<tenant>.bamboohr.com`) 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)