{"record":{"id":"72886175aca895f7","repo":"santifer/career-ops","slug":"manfred-invalid-url-url","errorCode":null,"errorMessage":"manfred: invalid URL: ${url}","messagePattern":"manfred: invalid URL: (.+?)","errorType":"validation","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"providers/manfred.mjs","lineNumber":41,"sourceCode":"\nconst FEED_BASE = 'https://www.getmanfred.com/api/v2/public/offers';\n// The full catalogue in one response runs ~2.3MB and takes 8-9.6s to arrive\n// even on a clean connection — right against the shared 10s default timeout,\n// so any network jitter aborts it. Give it real headroom rather than relying\n// on retry alone to paper over a structurally near-timeout request.\nconst FETCH_TIMEOUT_MS = 25_000;\nconst TRUSTED_HOST = 'www.getmanfred.com';\nconst OFFER_BASE = 'https://www.getmanfred.com/ofertas-empleo';\nconst VALID_LANGS = ['EN', 'ES'];\nconst DEFAULT_LANG = 'EN';\n\n/** @param {string} url */\nfunction assertManfredUrl(url) {\n  let parsed;\n  try {\n    parsed = new URL(url);\n  } catch {\n    throw new Error(`manfred: invalid URL: ${url}`);\n  }\n  if (parsed.protocol !== 'https:') throw new Error(`manfred: URL must use HTTPS: ${url}`);\n  if (parsed.hostname !== TRUSTED_HOST) {\n    throw new Error(`manfred: untrusted hostname \"${parsed.hostname}\" — must be ${TRUSTED_HOST}`);\n  }\n  return url;\n}\n\n/** Resolve the feed language: `lang` on the entry, uppercased, else EN. */\nexport function resolveLang(entry) {\n  const raw = typeof entry?.lang === 'string' ? entry.lang.trim().toUpperCase() : '';\n  return VALID_LANGS.includes(raw) ? raw : DEFAULT_LANG;\n}\n\n// The feed reports currency as the SYMBOL, not an ISO code, and the observed\n// values include a narrow-no-break-space variant of the euro sign. scan.mjs's\n// salary_filter compares currencies case-insensitively as plain strings, so a\n// symbol would never match a user's `currency: EUR` — map to ISO, and drop the","sourceCodeStart":23,"sourceCodeEnd":59,"githubUrl":"https://github.com/santifer/career-ops/blob/e7abd431fce9348a95261acac9e0c14779c35df8/providers/manfred.mjs#L23-L59","documentation":"assertManfredUrl() validates every URL before the Manfred provider fetches it. It first runs the string through the URL constructor; if the string cannot be parsed as a URL at all, this error is thrown with the offending value embedded in the message. The provider throws it eagerly so bad config fails fast instead of producing a confusing network error later.","triggerScenarios":"Calling the Manfred provider (fetch entry point, which calls assertManfredUrl(buildFeedUrl(entry))) with a careers_url/feed URL that is not a parseable URL — e.g. an empty string, 'getmanfred.com/feed' missing the scheme, or a value containing spaces or unencoded characters.","commonSituations":"A typo'd or hand-edited careers_url in portals.yml; a config value read from an env var that is unset or truncated; a template placeholder like ${URL} left unsubstituted; passing a relative path where an absolute URL is required.","solutions":["Fix the URL in the provider entry config (portals.yml) so it is a fully-qualified absolute URL, e.g. https://getmanfred.com/...","Run 'new URL(value)' on the configured value locally to confirm it parses before wiring it in.","If the value comes from an env var or template, verify it is set and substituted (no empty string, no literal ${...} placeholder).","If the URL is built dynamically by buildFeedUrl, log the interpolated value and check which segment injects the invalid part."],"exampleFix":"// before\ncareers_url: getmanfred.com/api/feed\n// after\ncareers_url: https://getmanfred.com/api/feed","handlingStrategy":"validation","validationCode":"function isValidUrl(u) { try { new URL(u); return true; } catch { return false; } }\nif (!isValidUrl(entry.careers_url)) throw new TypeError(`entry ${entry.name}: careers_url is not a valid absolute URL`);","typeGuard":"const isUrl = (v) => typeof v === 'string' && v.length > 0 && !Number.isNaN(Date.parse('x')) && (() => { try { new URL(v); return true; } catch { return false; } })();","tryCatchPattern":"try {\n  await provider.fetch(entry, ctx);\n} catch (err) {\n  if (String(err.message).startsWith('manfred: invalid URL')) {\n    console.error(`Bad careers_url for ${entry.name}: ${entry.careers_url}`);\n    return;\n  }\n  throw err;\n}","preventionTips":["Keep careers_url values fully-qualified with https:// in portals.yml.","Validate all entry URLs at config-load time with new URL() so failures surface before any scan work.","Avoid interpolating unset env vars into URLs — assert env presence before building the URL.","Run a config lint (verify-pipeline.mjs) before scanning to catch placeholder values like ${URL}."],"tags":["url","validation","config"],"backgroundTag":"invalid-url","analyzedSha":"e7abd431fce9348a95261acac9e0c14779c35df8","analyzedAt":"2026-09-16T06:35:29.214Z","contentChangedAt":"2026-09-16T06:35:29.214Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}