{"record":{"id":"77610c5b9647ed01","repo":"santifer/career-ops","slug":"nofluffjobs-careers-url-or-api-must-be-a-trusted","errorCode":null,"errorMessage":"nofluffjobs: careers_url or api must be a trusted nofluffjobs.com URL","messagePattern":"nofluffjobs: careers_url or api must be a trusted nofluffjobs\\.com URL","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"providers/nofluffjobs.mjs","lineNumber":138,"sourceCode":"        url: `${JOB_BASE}${slug}`,\n        company,\n        location: normalizeLocation(posting),\n        postedAt: postedAtMillis(posting.posted),\n      };\n    })\n    .filter(Boolean);\n}\n\n/** @type {Provider} */\nexport default {\n  id: 'nofluffjobs',\n\n  detect(entry) {\n    return detectUrl(entry);\n  },\n\n  async fetch(entry, ctx) {\n    if (!detectUrl(entry)) throw new Error('nofluffjobs: careers_url or api must be a trusted nofluffjobs.com URL');\n\n    const maxPages = Number(entry.max_pages || MAX_PAGES);\n    const jobs = [];\n    const seen = new Set();\n\n    for (let pageTo = 1; pageTo <= maxPages; pageTo++) {\n      const { url, body } = buildRequest(entry, pageTo);\n      const json = await ctx.fetchJson(url, {\n        method: 'POST',\n        body: JSON.stringify(body),\n        headers: {\n          accept: 'application/json, text/plain, */*',\n          'content-type': 'application/infiniteSearch+json',\n        },\n        redirect: 'error',\n      });\n      for (const job of parseNoFluffJobsResponse(json)) {\n        if (seen.has(job.url)) continue;","sourceCodeStart":120,"sourceCodeEnd":156,"githubUrl":"https://github.com/santifer/career-ops/blob/9b17a8ac97b398a496b38e423ae24e433b43254f/providers/nofluffjobs.mjs#L120-L156","documentation":"Thrown by nofluffjobs' fetch() when detectUrl(entry) returns null — meaning the entry has no usable api or careers_url that passes the full SSRF guard chain. This is a config-completeness check: fetch() re-validates detect()'s preconditions and refuses to proceed if the entry cannot resolve to a trusted URL. It prevents fetch() from silently doing nothing or hitting an unvalidated URL.","triggerScenarios":"detectUrl() returns null when: (1) entry.api and entry.careers_url are both empty/non-string; (2) the URL fails assertNoFluffUrl (wrong scheme, wrong host, unparseable) and detectUrl's catch returns null. If detect() returned non-null but the entry was mutated between detect() and fetch(), or if fetch() is called directly with an unvalidated entry, this fires.","commonSituations":"A portals.yml entry is missing both api and careers_url fields. The entry has a URL that was valid for a different provider (wrong host). The entry object was constructed programmatically (e.g. by a batch script) without the required URL field. A race condition or config reload changes the entry between detect() and fetch().","solutions":["Check the portals.yml entry: it must have either api or careers_url set to a valid https://nofluffjobs.com URL.","Ensure detect() and fetch() receive the same entry object — if entries are being rebuilt or merged, verify the nofluffjobs entry survives the process.","If calling fetch() programmatically, call detect(entry) first and skip entries that return null."],"exampleFix":"// before — entry has no usable URL\njob_boards:\n  nofluff:\n    provider: nofluffjobs\n    # missing api and careers_url\n\n// after\njob_boards:\n  nofluff:\n    provider: nofluffjobs\n    api: 'https://nofluffjobs.com/api/search/posting'\n    max_pages: 5","handlingStrategy":"validation","validationCode":"/** Verify the entry can resolve to a trusted NoFluffJobs URL before fetch. */\nfunction canFetchNoFluff(entry) {\n  const url = entry.api || entry.careers_url || '';\n  if (typeof url !== 'string' || !url.trim()) return false;\n  try {\n    const p = new URL(url);\n    return p.protocol === 'https:' && p.hostname === 'nofluffjobs.com';\n  } catch {\n    return false;\n  }\n}\n\nif (!canFetchNoFluff(entry)) {\n  console.warn(`nofluffjobs entry ${entry.name} has no valid URL — skipping`);\n  continue;\n}","typeGuard":"/** @param {import('./_types.js').PortalEntry} entry @returns {boolean} */\nfunction hasValidNoFluffUrl(entry) {\n  const url = entry.api || entry.careers_url;\n  return typeof url === 'string' && url.startsWith('https://nofluffjobs.com');\n}","tryCatchPattern":"try {\n  await nofluffProvider.fetch(entry, ctx);\n} catch (err) {\n  if (String(err.message).includes('must be a trusted nofluffjobs.com URL')) {\n    console.warn(`skipping ${entry.name}: no valid nofluffjobs URL in config`);\n    continue;\n  }\n  throw err;\n}","preventionTips":["Call detect(entry) before fetch() and skip entries returning null.","Validate portals.yml entries at load time for required URL fields per provider.","Ensure config merges preserve the api/careers_url field for nofluffjobs entries."],"tags":["config-validation","ssrf-guard","nofluffjobs","config"],"backgroundTag":null,"analyzedSha":"9b17a8ac97b398a496b38e423ae24e433b43254f","analyzedAt":"2026-08-13T00:48:39.135Z","schemaVersion":2},"datasetVersion":"2026-08-13T04:17:16.726Z"}