santifer/career-ops · error · Error

hecklerkoch: cannot resolve vacancy list for ${entry.name}

Error message

hecklerkoch: cannot resolve vacancy list for ${entry.name}

What it means

Thrown by hecklerkoch fetch() when resolveListUrl(entry) returns null. resolveListUrl accepts only http(s) URLs whose host is exactly heckler-koch.com or a *.heckler-koch.com subdomain, and returns null for any other host, a non-URL string, or a non-http(s) scheme. detect() returns null silently for the same condition; fetch() turns it into a hard error.

Source

Thrown at providers/hecklerkoch.mjs:86

    seen.add(id);
    out.push({ id, title, url });
  }
  return out;
}

/** @type {Provider} */
export default {
  id: 'hecklerkoch',

  detect(entry) {
    const url = entry.api || entry.careers_url || '';
    if (typeof url !== 'string') return null;
    return resolveListUrl({ api: url }) ? { url } : null;
  },

  async fetch(entry, ctx) {
    const listUrl = resolveListUrl(entry);
    if (!listUrl) throw new Error(`hecklerkoch: cannot resolve vacancy list for ${entry.name}`);
    const html = await ctx.fetchText(listUrl, { headers: { accept: 'text/html' } });
    const rows = parseListing(html);
    const jobs = [];
    for (const row of rows) {
      jobs.push({ title: row.title, url: row.url, company: entry.name, location: '' });
      if (jobs.length >= MAX_JOBS) break;
    }
    return jobs;
  },
};

View on GitHub (pinned to 9b17a8ac97)

Solutions

  1. Set careers_url (or api) to https://heckler-koch.com/de/Karriere/Stellenangebote or any https://*.heckler-koch.com URL.
  2. If the entry is not actually Heckler & Koch, remove provider: hecklerkoch and let detect() route it to the right provider.
  3. Confirm with detect(): if provider.detect(entry) returns null, fetch() will throw this too.

Example fix

# before
- name: Heckler & Koch
  provider: hecklerkoch
  careers_url: https://www.indeed.com/jobs?q=hk

# after
- name: Heckler & Koch
  provider: hecklerkoch
  careers_url: https://heckler-koch.com/de/Karriere/Stellenangebote
Defensive patterns

Strategy: validation

Validate before calling

const hit = hecklerkochProvider.detect(entry);
if (!hit) {
  console.warn(`hecklerkoch: ${entry.name} — careers_url must be on heckler-koch.com or a subdomain`);
  continue;
}
const jobs = await hecklerkochProvider.fetch(entry, ctx);

Type guard

/** True when the entry can resolve an H&K vacancy list URL. */
function isHecklerkochDerivable(entry) {
  const raw = entry?.api || entry?.careers_url;
  if (typeof raw !== 'string') return false;
  try {
    const u = new URL(raw);
    const host = u.host.toLowerCase();
    return (u.protocol === 'https:' || u.protocol === 'http:')
      && (host === 'heckler-koch.com' || host.endsWith('.heckler-koch.com'));
  } catch { return false; }
}

Try / catch

try {
  const jobs = await hecklerkochProvider.fetch(entry, ctx);
} catch (err) {
  if (/cannot resolve vacancy list/.test(err.message)) {
    console.error(`config: ${entry.name} — ${err.message}`);
  } else {
    throw err;
  }
}

Prevention

When it happens

Trigger: An entry tagged provider: hecklerkoch whose api/careers_url points at a different host (e.g. the karriere.heckler-koch.com apply backend, which is a subdomain but the path may not be a Stellenangebote page — note resolveListUrl still accepts the subdomain and rewrites the path, so this is fine; the real trigger is a wholly different host); a careers_url that is not a string; a URL with an unsupported scheme like ftp.

Common situations: Wiring the apply-backend URL (karriere.heckler-koch.com) as careers_url expecting it to list jobs — it won't, but resolveListUrl still rewrites it to the Stellenangebote path so this should pass; pasting a third-party recruiter link; setting provider: hecklerkoch on a generic company that is not H&K.

Related errors


AI-assisted analysis of santifer/career-ops@9b17a8ac97 (2026-08-13). Data as JSON: /api/errors/64f136911629919c. Report an issue: GitHub.