{"record":{"id":"840d95ec0d7bc738","repo":"santifer/career-ops","slug":"teamtailor-cannot-derive-jobs-rss-url-for-fallb","errorCode":null,"errorMessage":"teamtailor: cannot derive jobs.rss URL for ${fallbackCompany(entry)}","messagePattern":"teamtailor: cannot derive jobs\\.rss URL for (.+?)","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"providers/teamtailor.mjs","lineNumber":87,"sourceCode":"\nfunction fallbackCompany(entry) {\n  return typeof entry?.name === 'string' && entry.name.trim() ? entry.name.trim() : 'Teamtailor';\n}\n\n/** @type {Provider} */\nexport default {\n  id: 'teamtailor',\n\n  detect(entry) {\n    // Auto-detection never claims a branded host — only *.teamtailor.com.\n    const url = resolveFeedUrl(entry, { explicit: false });\n    return url ? { url } : null;\n  },\n\n  async fetch(entry, ctx) {\n    const explicit = entry?.provider === 'teamtailor';\n    const feedUrl = resolveFeedUrl(entry, { explicit });\n    if (!feedUrl) throw new Error(`teamtailor: cannot derive jobs.rss URL for ${fallbackCompany(entry)}`);\n    assertFeedUrl(feedUrl, { explicit });\n    // redirect:'error' prevents SSRF via server-side redirects; combined with\n    // assertFeedUrl above it keeps the request pinned to the trusted host\n    // (*.teamtailor.com, or the branded host the user explicitly configured).\n    const text = await ctx.fetchText(feedUrl, { redirect: 'error' });\n    return parseTeamtailorFeed(text, fallbackCompany(entry));\n  },\n};\n\n// Resolve a tag's inner text: unwrap a CDATA section, else decode entities.\nfunction extractText(inner) {\n  const cdata = inner.match(/^\\s*<!\\[CDATA\\[([\\s\\S]*?)\\]\\]>\\s*$/);\n  if (cdata) return cdata[1].trim();\n  return decodeEntities(inner).trim();\n}\n\n// Extract the text of the first <tag>...</tag> in a block. Returns '' when\n// absent. Tag names may contain a namespace colon (e.g. tt:city).","sourceCodeStart":69,"sourceCodeEnd":105,"githubUrl":"https://github.com/santifer/career-ops/blob/aac998c7ed7248ea853b720ceeb1fdbeb322fc5d/providers/teamtailor.mjs#L69-L105","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Set careers_url to the company's teamtailor feed URL (e.g. https://<slug>.teamtailor.com/jobs.rss)","Or add provider: teamtailor plus the branded host URL explicitly","Verify the company actually uses TeamTailor; otherwise switch providers"],"exampleFix":"// before\n- name: Acme\n# after\n- name: Acme\n  careers_url: 'https://acme.teamtailor.com/jobs.rss'","handlingStrategy":"validation","validationCode":"if (String(entry.provider ?? '') !== 'teamtailor' && !entry.careers_url) {\n  throw new Error(`${entry.name}: teamtailor auto-derivation needs careers_url`);\n}","typeGuard":"function canDeriveTTFeed(entry) {\n  return Boolean(entry?.provider === 'teamtailor' || (typeof entry?.careers_url === 'string' && entry.careers_url.startsWith('http')));\n}","tryCatchPattern":"try {\n  offers = await provider.fetch(entry, ctx);\n} catch (e) {\n  if (e.message.includes('cannot derive jobs.rss URL')) {\n    console.warn(`${entry.name}: add a careers_url pointing at the teamtailor feed`);\n    return [];\n  }\n  throw e;\n}","preventionTips":["Always fill careers_url for teamtailor entries, even when auto-detection seems possible","Confirm the company actually hosts on TeamTailor before selecting the provider","Keep per-provider example entries in your config templates"],"tags":["config","missing-field","rss"],"backgroundTag":"missing-required-config-field","analyzedSha":"aac998c7ed7248ea853b720ceeb1fdbeb322fc5d","analyzedAt":"2026-09-16T06:35:29.214Z","contentChangedAt":"2026-09-16T06:35:29.214Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}