{"record":{"id":"4528aff245591b42","repo":"santifer/career-ops","slug":"justjoin-invalid-url-url","errorCode":null,"errorMessage":"justjoin: invalid URL: ${url}","messagePattern":"justjoin: invalid URL: (.+?)","errorType":"validation","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"providers/justjoin.mjs","lineNumber":19,"sourceCode":"// @ts-check\n/** @typedef {import('./_types.js').Provider} Provider */\n\n// JustJoin.it provider — hits the current candidate offers API.\n// Browser URLs under https://justjoin.it/job-offers/... are accepted for\n// detection, but fetches use https://justjoin.it/api/candidate-api/offers.\n\nconst ALLOWED_HOSTS = new Set(['justjoin.it']);\nconst API_BASE = 'https://justjoin.it/api/candidate-api/offers';\nconst JOB_BASE = 'https://justjoin.it/job-offer/';\nconst PAGE_SIZE = 100;\nconst MAX_PAGES = 50;\n\nfunction assertJustJoinUrl(url) {\n  let parsed;\n  try {\n    parsed = new URL(url);\n  } catch {\n    throw new Error(`justjoin: invalid URL: ${url}`);\n  }\n  if (parsed.protocol !== 'https:') throw new Error(`justjoin: URL must use HTTPS: ${url}`);\n  if (!ALLOWED_HOSTS.has(parsed.hostname)) {\n    throw new Error(`justjoin: untrusted hostname \"${parsed.hostname}\" — must be justjoin.it`);\n  }\n  if (!parsed.pathname.startsWith('/job-offers') && parsed.pathname !== '/api/candidate-api/offers') {\n    throw new Error(`justjoin: URL path must be /job-offers or /api/candidate-api/offers: ${url}`);\n  }\n  return parsed;\n}\n\nfunction detectUrl(entry) {\n  const url = entry.api || entry.careers_url || '';\n  if (typeof url !== 'string' || !url.trim()) return null;\n  try {\n    const parsed = assertJustJoinUrl(url);\n    return { url: parsed.href };\n  } catch {","sourceCodeStart":1,"sourceCodeEnd":37,"githubUrl":"https://github.com/santifer/career-ops/blob/aac998c7ed7248ea853b720ceeb1fdbeb322fc5d/providers/justjoin.mjs#L1-L37","documentation":"assertJustJoinUrl in the JustJoin.it provider throws this when new URL(url) itself fails — i.e. the string is not a parseable absolute URL (missing scheme, whitespace, garbage). It is a fail-fast input validation guarding fetches so only well-formed https justjoin.it URLs ever reach the network layer. Related but distinct throws handle HTTPS violations, untrusted hostnames, and bad paths; this one is purely about URL parseability.","triggerScenarios":"Raised wherever assertJustJoinUrl runs on entry input: buildApiUrl(entry, from) with entry.api set to a relative string ('/api/offers'), a URL missing the scheme ('justjoin.it/api/candidate-api/offers'), containing spaces or control characters, or entry.careers_url being a non-URL string passed straight through; also detect(), though detect catches the throw internally and returns null.","commonSituations":"Portal entry in portals.yml has api: 'justjoin.it/api/candidate-api/offers' (no https://); a copy-pasted URL carries trailing whitespace/newline or surrounding quotes; someone configured a relative path assuming the base is implied; an environment variable or template placeholder left unfilled so the value is a literal like '${API_URL}'.","solutions":["Prefix the scheme in the portal entry: set api/careers_url to the full 'https://justjoin.it/api/candidate-api/offers' (or an https://justjoin.it/job-offers/... URL)","Trim whitespace and strip wrapping quotes from the configured URL before it reaches the provider","Validate the entry config at load time (e.g. new URL(entry.api ?? entry.careers_url)) so a malformed URL fails at startup with a clear config message rather than mid-scan","If the value comes from an env var or template, confirm it was substituted — a literal '${...}' placeholder will never parse","Check portals.yml for typos (colon-slash count, protocol spelling) in the justjoin entry"],"exampleFix":"// before (portals.yml entry)\ncompanies:\n  - name: example\n    api: justjoin.it/api/candidate-api/offers\n// after\ncompanies:\n  - name: example\n    api: https://justjoin.it/api/candidate-api/offers","handlingStrategy":"validation","validationCode":"function isValidJustJoinUrl(url) {\n  if (typeof url !== 'string') return false;\n  let parsed;\n  try { parsed = new URL(url.trim()); } catch { return false; }\n  return parsed.protocol === 'https:'\n    && parsed.hostname === 'justjoin.it'\n    && (parsed.pathname.startsWith('/job-offers') || parsed.pathname === '/api/candidate-api/offers');\n}\n// in portals.yml loading: if (!isValidJustJoinUrl(entry.api)) fail fast with the entry name;","typeGuard":"function isAbsoluteHttpUrl(value) {\n  if (typeof value !== 'string' || !value.trim()) return false;\n  try { const u = new URL(value.trim()); return u.protocol === 'https:' || u.protocol === 'http:'; }\n  catch { return false; }\n}","tryCatchPattern":"try {\n  const offers = await justjoinProvider.fetch(entry, ctx);\n} catch (err) {\n  if (String(err.message).includes('invalid URL')) {\n    console.error(`justjoin entry ${entry.name} has a malformed api/careers_url: ${err.message} — fix portals.yml`);\n    return [];\n  }\n  throw err;\n}","preventionTips":["Always configure api/careers_url as a fully-qualified https://justjoin.it/... URL — never relative or scheme-less","Trim and de-quote configured values when loading portals.yml","Validate all portal entries with new URL() at config load time so bad URLs fail at startup, not mid-scan","Check for unsubstituted template placeholders (${...}) when URLs come from env or templating","Remember the provider only trusts host justjoin.it — redirects to other domains will be rejected by sibling guards"],"tags":["validation","url","config"],"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"}