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

  1. 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).
  2. Open the company's MokaHR careers page and copy the site ID from the URL/network tab rather than guessing it.
  3. 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.
  4. 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

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


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)