{"record":{"id":"6de7395a9a37a849","repo":"santifer/career-ops","slug":"wttj-invalid-url-url","errorCode":null,"errorMessage":"wttj: invalid URL: ${url}","messagePattern":"wttj: invalid URL: (.+?)","errorType":"validation","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"providers/wttj.mjs","lineNumber":63,"sourceCode":"const INDEX = 'wttj_jobs_production_en';\nconst DEFAULT_MAX_HITS = 100;\nconst MAX_HITS_CAP = 200;\n// An unfiltered keyword query matches a large slice of a global board — \"product\n// manager\" alone returns ~14k hits — so Algolia's own relevance ranking, not the\n// scanner's filters, decides which 200 are seen. A server-side `filters`\n// expression cuts the result set to something a single request can actually\n// exhaust (e.g. product-management + France + full_time is ~450), so the cap is\n// raised to Algolia's per-request ceiling for this index when one is configured.\nconst FILTERED_MAX_HITS_CAP = 1000;\nconst FILTERS_MAX_LEN = 1000;\n\n/** Pin a URL to an expected https host. */\nfunction assertHost(url, host, label) {\n  let parsed;\n  try {\n    parsed = new URL(url);\n  } catch {\n    throw new Error(`wttj: invalid URL: ${url}`);\n  }\n  if (parsed.protocol !== 'https:') throw new Error(`wttj: URL must use HTTPS: ${url}`);\n  if (parsed.hostname !== host.toLowerCase()) {\n    throw new Error(`wttj: untrusted ${label} hostname \"${parsed.hostname}\" — must be ${host}`);\n  }\n  return url;\n}\n\n/**\n * Parse the `window.env = {...}` payload served by /api/env and extract the\n * Algolia application id + client search key.\n * @param {string} text\n * @returns {{ appId: string, apiKey: string }}\n */\nexport function parseEnvPayload(text) {\n  const start = text.indexOf('{');\n  const end = text.lastIndexOf('}');\n  if (start === -1 || end <= start) throw new Error('wttj: /api/env payload has no JSON object');","sourceCodeStart":45,"sourceCodeEnd":81,"githubUrl":"https://github.com/santifer/career-ops/blob/aac998c7ed7248ea853b720ceeb1fdbeb322fc5d/providers/wttj.mjs#L45-L81","documentation":"wttj's assertHost pins a URL to an expected HTTPS host. If the URL cannot be parsed by the URL constructor at all (malformed input), it throws 'invalid URL' before any protocol/host checks. This is the provider's first line of URL validation.","triggerScenarios":"assertHost called with a malformed string — missing scheme, spaces, unescaped characters, or a non-URL value passed where a URL is expected.","commonSituations":"A careers_url in config with a typo or missing protocol ('apply.wttj.io/jobs'); a truncated URL copied from a document; an interpolated URL built from an empty slug.","solutions":["Print/inspect the offending url string from the error message and fix its syntax","Add the https:// scheme if it is missing","Validate careers_url values in config before loading the provider"],"exampleFix":"// before\nconst url = 'apply.wttj.io/acme/jobs';\n// after\nconst url = 'https://apply.wttj.io/acme/jobs';","handlingStrategy":"validation","validationCode":"function isParseableUrl(u) { try { new URL(u); return true; } catch { return false; } }\nif (!isParseableUrl(entry.careers_url)) throw new Error(`bad careers_url: ${entry.careers_url}`);","typeGuard":"const isParseableUrl = (u) => { try { new URL(u); return typeof u === 'string' && u.length > 0; } catch { return false; } };","tryCatchPattern":"try { return wttjFetch(url); } catch (e) { if (e.message.startsWith('wttj: invalid URL')) { console.error(`Malformed URL in config: ${e.message}`); return null; } throw e; }","preventionTips":["Validate all configured URLs with new URL() at config load time","Never build URLs by raw string concatenation without encoding","Require an explicit https:// scheme in config values"],"tags":["url-validation","malformed-url","wttj","provider-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"}