santifer/career-ops · error
flowxtra: invalid URL
Error message
flowxtra: invalid URL: ${url} What it means
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.
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.
Example fix
// before const JOBS_ENDPOINT = process.env.FLOWXTRA_URL; // '' when unset → invalid URL // after const JOBS_ENDPOINT = process.env.FLOWXTRA_URL || 'https://app.flowxtra.com/api/central/jobs';
Defensive patterns
Strategy: validation
Validate before calling
function isFlowxtraEndpoint(url) {
try {
const u = new URL(url);
return u.protocol === 'https:' && u.hostname === 'app.flowxtra.com';
} catch { return false; }
}
// call before fetching: if (!isFlowxtraEndpoint(url)) throw new Error(...); Type guard
const isTrustedFlowxtraHost = (url) => { try { return new URL(url).hostname === 'app.flowxtra.com'; } catch { return false; } }; Try / catch
let jobs = [];
try {
jobs = await provider.fetch(entry, ctx);
} catch (err) {
if (String(err.message).startsWith('flowxtra: invalid URL')) {
console.error('JOBS_ENDPOINT is malformed — restore https://app.flowxtra.com/api/central/jobs');
} else {
throw err;
}
} Prevention
- 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
When it happens
Trigger: 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.
Common situations: 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.
Understand the failure class
Background: "Invalid URL" / "URL cannot be empty": fix the malformed or missing URL behind request-construction failures — this error's family across 50 libraries.
Related errors
- eightfold: untrusted hostname
- feishu-jobs: careers_url must use HTTPS on…
- a16z-speedrun-talent: invalid URL
- Access denied: Localhost or internal domain target detected.
- agentic-jobs: invalid URL
AI-assisted analysis of santifer/career-ops@aac998c7ed (2026-09-16).
Data as JSON: /api/errors/cf1cb3d2899379f1.
Report an issue: GitHub.
Appendix: source
Thrown at providers/flowxtra.mjs:31
// arbeitnow/echojobs/thehub, not a per-company provider: scan.mjs's own
// title/location filters narrow the result afterwards.
//
// Wire in via a `job_boards:` entry with `provider: flowxtra`.
const JOBS_ENDPOINT = 'https://app.flowxtra.com/api/central/jobs';
const TRUSTED_ENDPOINT_HOST = 'app.flowxtra.com';
const TRUSTED_APPLY_HOST = 'flowxtra.com';
const PER_PAGE = 100;
const DEFAULT_MAX_PAGES = 3;
const MAX_PAGES_CAP = 50;
/** @param {string} url */
function assertFlowxtraEndpointUrl(url) {
let parsed;
try {
parsed = new URL(url);
} catch {
throw new Error(`flowxtra: invalid URL: ${url}`);
}
if (parsed.protocol !== 'https:') throw new Error(`flowxtra: URL must use HTTPS: ${url}`);
if (parsed.hostname !== TRUSTED_ENDPOINT_HOST) {
throw new Error(`flowxtra: untrusted hostname "${parsed.hostname}" — must be ${TRUSTED_ENDPOINT_HOST}`);
}
return url;
}
/** Resolve the page cap: a positive integer `max_pages` on the entry, capped. */
function resolveMaxPages(entry) {
const v = entry?.max_pages;
if (Number.isInteger(v) && v > 0) return Math.min(v, MAX_PAGES_CAP);
return DEFAULT_MAX_PAGES;
}
// NaN-safe Date.parse — `|| undefined` would also coerce a valid epoch 0.
function toEpochMs(value) {
if (!value) return undefined;View on GitHub (pinned to aac998c7ed)