santifer/career-ops · error · Error

teamtailor: cannot derive jobs.rss URL for

Error message

teamtailor: cannot derive jobs.rss URL for ${fallbackCompany(entry)}

What it means

When an entry does not explicitly declare provider: teamtailor, the provider tries to auto-derive the feed URL (host normalized to /jobs.rss). If resolveFeedUrl returns null — the entry has no usable careers_url/host to derive from — the fetch cannot proceed and throws naming the company.

Solutions

  1. Set careers_url to the company's teamtailor feed URL (e.g. https://<slug>.teamtailor.com/jobs.rss)
  2. Or add provider: teamtailor plus the branded host URL explicitly
  3. Verify the company actually uses TeamTailor; otherwise switch providers

Example fix

// before
- name: Acme
# after
- name: Acme
  careers_url: 'https://acme.teamtailor.com/jobs.rss'
Defensive patterns

Strategy: validation

Validate before calling

if (String(entry.provider ?? '') !== 'teamtailor' && !entry.careers_url) {
  throw new Error(`${entry.name}: teamtailor auto-derivation needs careers_url`);
}

Type guard

function canDeriveTTFeed(entry) {
  return Boolean(entry?.provider === 'teamtailor' || (typeof entry?.careers_url === 'string' && entry.careers_url.startsWith('http')));
}

Try / catch

try {
  offers = await provider.fetch(entry, ctx);
} catch (e) {
  if (e.message.includes('cannot derive jobs.rss URL')) {
    console.warn(`${entry.name}: add a careers_url pointing at the teamtailor feed`);
    return [];
  }
  throw e;
}

Prevention

When it happens

Trigger: Entry missing careers_url and any URL-ish field so nothing can be normalized to /jobs.rss; careers_url is present but resolveFeedUrl's matching rejects it (empty, non-URL); entry.provider set to a different value so explicit=true logic is off while no URL exists.

Common situations: Newly added company with only a name filled in; company's careers site isn't TeamTailor-hosted so no feed can be derived; config migration dropped the url field.

Understand the failure class

Background: "is required", "must be set", "missing required field": configuration validation errors across open-source libraries — this error's family across 36 libraries.

Related errors


AI-assisted analysis of santifer/career-ops@aac998c7ed (2026-09-16). Data as JSON: /api/errors/840d95ec0d7bc738. Report an issue: GitHub.

Appendix: source

Thrown at providers/teamtailor.mjs:87

function fallbackCompany(entry) {
  return typeof entry?.name === 'string' && entry.name.trim() ? entry.name.trim() : 'Teamtailor';
}

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

  detect(entry) {
    // Auto-detection never claims a branded host — only *.teamtailor.com.
    const url = resolveFeedUrl(entry, { explicit: false });
    return url ? { url } : null;
  },

  async fetch(entry, ctx) {
    const explicit = entry?.provider === 'teamtailor';
    const feedUrl = resolveFeedUrl(entry, { explicit });
    if (!feedUrl) throw new Error(`teamtailor: cannot derive jobs.rss URL for ${fallbackCompany(entry)}`);
    assertFeedUrl(feedUrl, { explicit });
    // redirect:'error' prevents SSRF via server-side redirects; combined with
    // assertFeedUrl above it keeps the request pinned to the trusted host
    // (*.teamtailor.com, or the branded host the user explicitly configured).
    const text = await ctx.fetchText(feedUrl, { redirect: 'error' });
    return parseTeamtailorFeed(text, fallbackCompany(entry));
  },
};

// Resolve a tag's inner text: unwrap a CDATA section, else decode entities.
function extractText(inner) {
  const cdata = inner.match(/^\s*<!\[CDATA\[([\s\S]*?)\]\]>\s*$/);
  if (cdata) return cdata[1].trim();
  return decodeEntities(inner).trim();
}

// Extract the text of the first <tag>...</tag> in a block. Returns '' when
// absent. Tag names may contain a namespace colon (e.g. tt:city).

View on GitHub (pinned to aac998c7ed)