{"record":{"id":"cf1cb3d2899379f1","repo":"santifer/career-ops","slug":"flowxtra-invalid-url-url","errorCode":null,"errorMessage":"flowxtra: invalid URL: ${url}","messagePattern":"flowxtra: invalid URL: (.+?)","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"providers/flowxtra.mjs","lineNumber":31,"sourceCode":"// arbeitnow/echojobs/thehub, not a per-company provider: scan.mjs's own\n// title/location filters narrow the result afterwards.\n//\n// Wire in via a `job_boards:` entry with `provider: flowxtra`.\n\nconst JOBS_ENDPOINT = 'https://app.flowxtra.com/api/central/jobs';\nconst TRUSTED_ENDPOINT_HOST = 'app.flowxtra.com';\nconst TRUSTED_APPLY_HOST = 'flowxtra.com';\nconst PER_PAGE = 100;\nconst DEFAULT_MAX_PAGES = 3;\nconst MAX_PAGES_CAP = 50;\n\n/** @param {string} url */\nfunction assertFlowxtraEndpointUrl(url) {\n  let parsed;\n  try {\n    parsed = new URL(url);\n  } catch {\n    throw new Error(`flowxtra: invalid URL: ${url}`);\n  }\n  if (parsed.protocol !== 'https:') throw new Error(`flowxtra: URL must use HTTPS: ${url}`);\n  if (parsed.hostname !== TRUSTED_ENDPOINT_HOST) {\n    throw new Error(`flowxtra: untrusted hostname \"${parsed.hostname}\" — must be ${TRUSTED_ENDPOINT_HOST}`);\n  }\n  return url;\n}\n\n/** Resolve the page cap: a positive integer `max_pages` on the entry, capped. */\nfunction resolveMaxPages(entry) {\n  const v = entry?.max_pages;\n  if (Number.isInteger(v) && v > 0) return Math.min(v, MAX_PAGES_CAP);\n  return DEFAULT_MAX_PAGES;\n}\n\n// NaN-safe Date.parse — `|| undefined` would also coerce a valid epoch 0.\nfunction toEpochMs(value) {\n  if (!value) return undefined;","sourceCodeStart":13,"sourceCodeEnd":49,"githubUrl":"https://github.com/santifer/career-ops/blob/aac998c7ed7248ea853b720ceeb1fdbeb322fc5d/providers/flowxtra.mjs#L13-L49","documentation":"assertFlowxtraEndpointUrl validates the board-wide jobs endpoint URL before each page fetch: it must parse as a URL, use HTTPS, and have hostname exactly app.flowxtra.com. This specific error fires when the input cannot be parsed by new URL() at all — syntactically invalid. In practice the URL is built internally from the JOBS_ENDPOINT constant, so hitting this means the constant was corrupted or the guard is being called with external/untrusted input.","triggerScenarios":"assertFlowxtraEndpointUrl receives a value that new URL() throws on: an empty string, undefined coerced to 'undefined', a URL with illegal characters or unencoded spaces, or a broken template literal (e.g. a page variable that interpolated as empty). With the shipped code the only call site builds the URL from the fixed JOBS_ENDPOINT, so a runtime throw here indicates tampering or a refactor regression.","commonSituations":"Someone localized JOBS_ENDPOINT to a relative path or env override that resolves to ''/undefined; a fork piped user-supplied page/base-URL config into the guard; tests invoking the guard directly with malformed strings to confirm it rejects them.","solutions":["Inspect the thrown message's url value and fix the source of the string — with stock code, restore JOBS_ENDPOINT to 'https://app.flowxtra.com/api/central/jobs'.","If JOBS_ENDPOINT was made configurable, validate the config at startup (must be an absolute https:// URL on app.flowxtra.com) before the fetch loop.","Check for template-literal mistakes where a variable interpolated as empty/undefined into the URL string.","If you truly need a different endpoint, note the guard also enforces the exact host app.flowxtra.com — fixing parseability alone will not be enough; the hostname check is next."],"exampleFix":"// before\nconst JOBS_ENDPOINT = process.env.FLOWXTRA_URL; // '' when unset → invalid URL\n// after\nconst JOBS_ENDPOINT = process.env.FLOWXTRA_URL || 'https://app.flowxtra.com/api/central/jobs';","handlingStrategy":"validation","validationCode":"function isFlowxtraEndpoint(url) {\n  try {\n    const u = new URL(url);\n    return u.protocol === 'https:' && u.hostname === 'app.flowxtra.com';\n  } catch { return false; }\n}\n// call before fetching: if (!isFlowxtraEndpoint(url)) throw new Error(...);","typeGuard":"const isTrustedFlowxtraHost = (url) => { try { return new URL(url).hostname === 'app.flowxtra.com'; } catch { return false; } };","tryCatchPattern":"let jobs = [];\ntry {\n  jobs = await provider.fetch(entry, ctx);\n} catch (err) {\n  if (String(err.message).startsWith('flowxtra: invalid URL')) {\n    console.error('JOBS_ENDPOINT is malformed — restore https://app.flowxtra.com/api/central/jobs');\n  } else {\n    throw err;\n  }\n}","preventionTips":["Do not make JOBS_ENDPOINT configurable without startup validation of scheme and host","Keep the endpoint as the shipped constant; the guard allows only app.flowxtra.com exactly","Validate any env/config override with new URL() + hostname check at boot, not per request","If you must fetch a mirror or proxy, add it to the trusted-host set deliberately rather than bypassing the guard"],"tags":["url-validation","ssrf","configuration","flowxtra"],"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"}