santifer/career-ops · error · Error
mokahr: careers_url must be an allowed HTTPS app.mokahr.com…
Error message
mokahr: careers_url must be an allowed HTTPS app.mokahr.com tenant URL with a positive site ID
What it means
The MokaHR provider's fetch() only accepts careers_url values that parse as an HTTPS URL on the app.mokahr.com host with a positive numeric site ID; parseTenantUrl() returns null otherwise and fetch() throws this error. It enforces both the host pin and the tenant-ID extraction contract before any request is made.
Solutions
- Set careers_url to the full HTTPS tenant URL on app.mokahr.com including its positive numeric site ID, e.g. https://app.mokahr.com/api/outer/9999 or the exact form parseTenantUrl expects (check providers/mokahr.mjs).
- Open the company's MokaHR careers page and copy the site ID from the URL/network tab rather than guessing it.
- If the tenant lives on a different MokaHR domain, that host is unsupported by this provider — request support for it rather than editing the check ad hoc.
- Validate the URL by running parseTenantUrl(careers_url) in a scratch node -e script and confirming it returns an object.
Example fix
// before careers_url: https://app.mokahr.com // after careers_url: https://app.mokahr.com/api/outer/12345
Defensive patterns
Strategy: validation
Validate before calling
const u = new URL(entry.careers_url);
const ok = u.protocol === 'https:' && u.hostname === 'app.mokahr.com' && /\d+/.test(u.pathname) && Number(u.pathname.match(/\d+/)[0]) > 0;
if (!ok) throw new Error(`${entry.name}: careers_url must be an app.mokahr.com tenant URL with a positive site ID`); Type guard
const isMokaTenantUrl = (v) => { try { const u = new URL(v); return u.protocol === 'https:' && u.hostname === 'app.mokahr.com' && Number(u.pathname.match(/\d+/)?.[0]) > 0; } catch { return false; } }; Try / catch
try {
await provider.fetch(entry, ctx);
} catch (err) {
if (String(err.message).startsWith('mokahr: careers_url must be')) {
console.error(`Fix careers_url for this entry: ${entry.careers_url}`);
return;
}
throw err;
} Prevention
- Copy tenant URLs directly from the company's live MokaHR careers page including the numeric site ID.
- Validate all careers_url values at config load (verify-pipeline.mjs) instead of mid-scan.
- Don't strip query/path segments when copying the URL — the site ID lives there.
- Document the expected URL form next to the entry in portals.yml.
When it happens
Trigger: entry.careers_url is missing/empty, uses http://, points at a non-app.mokahr.com host, lacks a site ID, or has a site ID that is zero/negative/non-numeric — e.g. careers_url: 'mokahr.com/mycompany' or 'https://app.mokahr.com' with no ID segment.
Common situations: A hand-edited portals.yml entry missing the tenant path; copying a frontend URL whose ID lives in a query/fragment the parser doesn't read; a company that uses MokaHR's other regional domains; an empty careers_url after a config migration.
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
- bamboohr: invalid URL
- breezy: invalid URL
- builtin: invalid URL
AI-assisted analysis of santifer/career-ops@aac998c7ed (2026-09-16).
Data as JSON: /api/errors/91dee1c91fc1f4bd.
Report an issue: GitHub.
Appendix: source
Thrown at providers/mokahr.mjs:212
}
return jobs;
}
/** @type {Provider} */
export default {
id: 'mokahr',
detect(entry) {
const url = entry.careers_url;
if (typeof url !== 'string') return null;
if (!parseTenantUrl(url)) return null;
return { url };
},
async fetch(entry, ctx) {
const tenant = parseTenantUrl(entry.careers_url);
if (!tenant) {
throw new Error('mokahr: careers_url must be an allowed HTTPS app.mokahr.com tenant URL with a positive site ID');
}
const keywords = Array.isArray(entry.keywords) && entry.keywords.length
? entry.keywords
: DEFAULT_KEYWORDS;
const entryMaxPages = Number(entry.max_pages) > 0 ? Number(entry.max_pages) : DEFAULT_MAX_PAGES;
const maxPages = Math.min(entryMaxPages, Number(ctx?.maxPages) > 0 ? Number(ctx.maxPages) : Infinity);
/** @type {Map<string, import('./_types.js').Job>} */
const seen = new Map();
const sleep = (ms) => (typeof ctx?.sleep === 'function' ? ctx.sleep(ms) : new Promise((r) => setTimeout(r, ms)));
let firstRequest = true;
let succeededOnce = false;
for (const keyword of keywords) {
for (let page = 1; page <= maxPages; page++) {
if (firstRequest) firstRequest = false;
else await sleep(INTER_PAGE_DELAY_MS);View on GitHub (pinned to aac998c7ed)