{"record":{"id":"8dd31ba274c30c28","repo":"santifer/career-ops","slug":"remotli-invalid-url-url","errorCode":null,"errorMessage":"remotli: invalid URL: ${url}","messagePattern":"remotli: invalid URL: (.+?)","errorType":"validation","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"providers/remotli.mjs","lineNumber":239,"sourceCode":"  const description = htmlToText(job.description);\n  if (description) out.description = description;\n\n  const postedAt = toEpochMs(job.publishedAt || job.createdAt);\n  if (postedAt !== undefined) out.postedAt = postedAt;\n\n  const salary = resolveSalary(job);\n  if (salary) out.salary = salary;\n\n  return out;\n}\n\n/** Guard the API URL: HTTPS + remotli.ch only. */\nfunction assertRemotliUrl(url) {\n  let parsed;\n  try {\n    parsed = new URL(url);\n  } catch {\n    throw new Error(`remotli: invalid URL: ${url}`);\n  }\n  if (parsed.protocol !== 'https:') throw new Error(`remotli: URL must use HTTPS: ${url}`);\n  if (!HOST_RE.test(parsed.hostname))\n    throw new Error(`remotli: untrusted hostname \"${parsed.hostname}\" — must be remotli.ch`);\n  return url;\n}\n\n/** @type {Provider} */\nexport default {\n  id: 'remotli',\n\n  detect(entry) {\n    const raw = typeof entry.careers_url === 'string' ? entry.careers_url : '';\n    if (!raw) return null;\n    let parsed;\n    try {\n      parsed = new URL(raw);\n    } catch {","sourceCodeStart":221,"sourceCodeEnd":257,"githubUrl":"https://github.com/santifer/career-ops/blob/aac998c7ed7248ea853b720ceeb1fdbeb322fc5d/providers/remotli.mjs#L221-L257","documentation":"assertRemotliUrl() guards the Remotli API URL before any server-side fetch: the URL must parse with the URL constructor, else this error is thrown (followed by HTTPS and remotli.ch-only host checks). It is part of the provider's SSRF defence.","triggerScenarios":"assertRemotliUrl() (via fetch or detect) receives an unparseable URL string — missing scheme, empty string, illegal characters, whitespace, or a placeholder value.","commonSituations":"A portals.yml entry stores the bare host (remotli.ch) without https://; copy-paste artifacts (spaces, quotes); an empty or template-placeholder careers_url field.","solutions":["Prefix the value with https:// if only a hostname was provided","Trim whitespace/quotes and correct typos in careers_url","Validate locally with new URL(raw) before committing the config"],"exampleFix":"// before\ncareers_url: remotli.ch/api\n// after\ncareers_url: https://remotli.ch/api","handlingStrategy":"validation","validationCode":"function isRemotliUrl(s) {\n  try { const u = new URL(s); return u.protocol === 'https:' && u.hostname === 'remotli.ch'; } catch { return false; }\n}\nif (!isRemotliUrl(entry.careers_url)) throw new Error(`careers_url must be https://remotli.ch/... for ${entry.name}`);","typeGuard":"function isValidHttpsUrl(v) {\n  try { return new URL(String(v)).protocol === 'https:'; } catch { return false; }\n}","tryCatchPattern":"try {\n  assertRemotliUrl(entry.careers_url);\n} catch (err) {\n  if (err.message.startsWith('remotli: invalid URL')) {\n    console.error(`Unparseable careers_url \"${entry.careers_url}\" — add https:// scheme and trim whitespace`);\n  }\n  throw err;\n}","preventionTips":["Always store fully qualified https:// URLs; never bare hostnames","Trim whitespace and quotes from copy-pasted config values","Locally run new URL(raw) as a pre-commit sanity check on careers_url fields"],"tags":["url-validation","config","ssrf-protection"],"backgroundTag":"invalid-url-format","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"}