{"record":{"id":"521492b53f9788db","repo":"santifer/career-ops","slug":"nodesk-invalid-url-url","errorCode":null,"errorMessage":"nodesk: invalid URL: ${url}","messagePattern":"nodesk: invalid URL: (.+?)","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"providers/nodesk.mjs","lineNumber":20,"sourceCode":"/** @typedef {import('./_types.js').Provider} Provider */\n\n// NoDesk provider - board-wide RSS feed\n// (https://nodesk.co/remote-jobs/index.xml). The feed is public, no-auth,\n// and XML, so it is parsed in-process with the same tiny tag extractor\n// approach as providers/personio.mjs rather than adding an XML dependency.\n//\n// Wire in via a `job_boards:` entry with `provider: nodesk`.\n\nconst FEED_URL = 'https://nodesk.co/remote-jobs/index.xml';\nconst TRUSTED_HOST = 'nodesk.co';\n\n/** @param {string} url */\nfunction assertNodeskUrl(url) {\n  let parsed;\n  try {\n    parsed = new URL(url);\n  } catch {\n    throw new Error(`nodesk: invalid URL: ${url}`);\n  }\n  if (parsed.protocol !== 'https:') throw new Error(`nodesk: URL must use HTTPS: ${url}`);\n  if (parsed.hostname !== TRUSTED_HOST) {\n    throw new Error(`nodesk: untrusted hostname \"${parsed.hostname}\" - must be ${TRUSTED_HOST}`);\n  }\n  return url;\n}\n\n// NaN-safe Date.parse - `|| undefined` would also coerce a valid epoch 0.\nfunction toEpochMs(value) {\n  if (!value) return undefined;\n  const parsed = Date.parse(value);\n  return Number.isNaN(parsed) ? undefined : parsed;\n}\n\nfunction fallbackCompany(entry) {\n  return typeof entry?.name === 'string' && entry.name.trim() ? entry.name.trim() : 'NoDesk';\n}","sourceCodeStart":2,"sourceCodeEnd":38,"githubUrl":"https://github.com/santifer/career-ops/blob/9b17a8ac97b398a496b38e423ae24e433b43254f/providers/nodesk.mjs#L2-L38","documentation":"Thrown by nodesk's assertNodeskUrl() when new URL(url) raises an exception — the URL string is syntactically unparseable by the WHATWG URL parser. This is the first gate in a three-stage SSRF guard (valid URL → HTTPS → trusted host) that keeps all fetches pinned to nodesk.co. It fires before any network request is made.","triggerScenarios":"Called with a value that new URL() cannot parse: empty string, undefined coerced to 'undefined', a URL with embedded spaces or control characters, a schemeless bare path like 'remote-jobs/index.xml', or a double-encoded malformed string. In practice this is reached when entry.api or a dynamically built URL is missing/malformed and bypasses an earlier detect() null-return.","commonSituations":"A portals.yml nodesk entry has careers_url or api set to an empty string, null, or a typo without a scheme (e.g. 'nodesk.co/remote-jobs/index.xml' without https://). Also occurs when a config migration script writes a non-string value that gets stringified to '[object Object]'. Since FEED_URL is a constant, this typically only fires if the constant is changed or assertNodeskUrl is called with a user-supplied URL.","solutions":["Inspect the value being passed to assertNodeskUrl — log or debug the url argument to see the exact malformed string.","If the URL comes from portals.yml, ensure it is a fully-qualified https:// URL (the provider uses the constant FEED_URL by default, so a configured override is the likely culprit).","If the constant FEED_URL itself was edited, restore it to 'https://nodesk.co/remote-jobs/index.xml'.","Add a pre-check in the caller: if (typeof url !== 'string' || !url.startsWith('http')) return null in detect() before reaching fetch()."],"exampleFix":"// before\nconst FEED_URL = 'https://nodesk.co/remote-jobs/index.xml';\n// if someone overrides to a bare path:\nassertNodeskUrl('nodesk.co/remote-jobs/index.xml'); // throws\n\n// after — ensure a scheme before parsing\nfunction assertNodeskUrl(url) {\n  const normalized = typeof url === 'string' && !url.match(/^[a-z]+:///i) ? `https://${url}` : url;\n  let parsed;\n  try { parsed = new URL(normalized); } catch {\n    throw new Error(`nodesk: invalid URL: ${url}`);\n  }\n  // ... rest of checks\n}","handlingStrategy":"validation","validationCode":"/** Validate a URL string is parseable before passing to assertNodeskUrl. */\nfunction isValidUrlString(url) {\n  return typeof url === 'string'\n    && url.length > 0\n    && /^https?:\\/\\/.+/i.test(url)\n    && (() => { try { new URL(url); return true; } catch { return false; } })();\n}\n\n// before calling the provider:\nif (!isValidUrlString(entry.api)) {\n  console.warn(`nodesk entry ${entry.name} has invalid URL, skipping`);\n  continue;\n}","typeGuard":"/** @param {unknown} url @returns {url is string} */\nfunction isParseableUrl(url) {\n  if (typeof url !== 'string' || !url) return false;\n  try { new URL(url); return true; } catch { return false; }\n}","tryCatchPattern":"try {\n  const jobs = await nodeskProvider.fetch(entry, ctx);\n} catch (err) {\n  if (String(err.message).startsWith('nodesk: invalid URL')) {\n    console.warn(`skipping nodesk entry ${entry.name}: malformed URL`);\n    continue;\n  }\n  throw err;\n}","preventionTips":["Validate all URLs in portals.yml at config-load time with a schema validator.","Always include the https:// scheme in portal entry URLs.","Use detect() to filter entries before calling fetch() — entries returning null from detect() should be skipped."],"tags":["url-validation","ssrf-guard","nodesk","config"],"backgroundTag":null,"analyzedSha":"9b17a8ac97b398a496b38e423ae24e433b43254f","analyzedAt":"2026-08-13T00:48:39.135Z","schemaVersion":2},"datasetVersion":"2026-08-13T04:17:16.726Z"}