{"record":{"id":"07c2b10ad85e51b0","repo":"santifer/career-ops","slug":"themuse-invalid-url-url","errorCode":null,"errorMessage":"themuse: invalid URL: ${url}","messagePattern":"themuse: invalid URL: (.+?)","errorType":"validation","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"providers/themuse.mjs","lineNumber":88,"sourceCode":"      const backoff = Math.min(RETRY_BASE_DELAY_MS * 2 ** attempt, RETRY_MAX_DELAY_MS);\n      // A server-supplied Retry-After is honored, but still clamped — an\n      // unbounded value would otherwise stall this board's fetch for as long\n      // as the server says, defeating the point of a bounded backoff.\n      const retryAfterMs = parseRetryAfterMs(err?.retryAfter);\n      const delayMs = retryAfterMs !== null ? Math.min(retryAfterMs, RETRY_MAX_DELAY_MS * 4) : (backoff + Math.random() * 250);\n      await sleep(delayMs, ctx);\n    }\n  }\n  throw lastErr;\n}\n\n/** @param {string} url */\nfunction assertMuseUrl(url) {\n  let parsed;\n  try {\n    parsed = new URL(url);\n  } catch {\n    throw new Error(`themuse: invalid URL: ${url}`);\n  }\n  if (parsed.protocol !== 'https:') throw new Error(`themuse: URL must use HTTPS: ${url}`);\n  if (parsed.hostname !== TRUSTED_HOST) {\n    throw new Error(`themuse: untrusted hostname \"${parsed.hostname}\" — must be ${TRUSTED_HOST}`);\n  }\n  return url;\n}\n\n/**\n * Normalize a single result from the Muse API response. Exported for unit tests.\n *\n * Field mapping:\n *   name              → title\n *   refs.landing_page → url\n *   company.name      → company\n *   locations[0].name → location\n *\n * Returns null when required fields (title or url) are missing or invalid.","sourceCodeStart":70,"sourceCodeEnd":106,"githubUrl":"https://github.com/santifer/career-ops/blob/aac998c7ed7248ea853b720ceeb1fdbeb322fc5d/providers/themuse.mjs#L70-L106","documentation":"assertMuseUrl() is the provider's URL validator: it first parses the input with the URL constructor. If parsing fails entirely (malformed URL string), it throws this error before any protocol or host checks. It guards every URL the provider uses so downstream fetches never see unparseable input.","triggerScenarios":"assertMuseUrl(url) is invoked with a string that new URL() rejects — missing scheme ('themuse.com/jobs'), spaces, invalid characters, an empty string, or a non-string coerced badly.","commonSituations":"A portals.yml entry with a careers_url missing the 'https://' prefix; trailing whitespace or smart quotes pasted from a document; an interpolated template producing an empty or partially-built URL.","solutions":["Fix the URL in the config so it is absolute and well-formed, e.g. 'https://www.themuse.com/api/v2/jobs?page=0'.","Trim whitespace and replace smart quotes with plain ASCII quotes in the config value.","Log the exact offending string (it is included in the message) and run new URL(value) in a REPL to see the syntax failure.","If the value is built dynamically, check the interpolation for undefined/empty parts."],"exampleFix":"// before\nurl: themuse.com/api/v2/jobs\n// after\nurl: https://www.themuse.com/api/v2/jobs","handlingStrategy":"validation","validationCode":"function isParseableUrl(value) {\n  if (typeof value !== 'string' || value.trim() === '') return false;\n  try { new URL(value.trim()); return true; } catch { return false; }\n}\n// run before invoking the provider: if (!isParseableUrl(entry.url)) fix config first","typeGuard":"const isUrl = (v) =>\n  typeof v === 'string'\n  && v.length > 0\n  && (() => { try { new URL(v); return true; } catch { return false; } })();","tryCatchPattern":"try {\n  return await provider.fetch(entry, ctx);\n} catch (err) {\n  if (String(err.message).startsWith('themuse: invalid URL')) {\n    console.error(`Config fix needed for ${entry.name}: ${err.message}`);\n    return null;\n  }\n  throw err;\n}","preventionTips":["Always store absolute URLs with an explicit https:// scheme in portals.yml","Trim and ASCII-normalize values copied from docs or chat before committing them to config","Validate the whole portals.yml with a small script (new URL on every URL field) as a pre-commit check","Never build URLs by string concatenation without checking the result parses"],"tags":["url","validation","themuse","config"],"backgroundTag":"invalid-url","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"}