santifer/career-ops · error · Error

manfred: invalid URL

Error message

manfred: invalid URL: ${url}

What it means

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.

Solutions

  1. Fix the URL in the provider entry config (portals.yml) so it is a fully-qualified absolute URL, e.g. https://getmanfred.com/...
  2. Run 'new URL(value)' on the configured value locally to confirm it parses before wiring it in.
  3. If the value comes from an env var or template, verify it is set and substituted (no empty string, no literal ${...} placeholder).
  4. If the URL is built dynamically by buildFeedUrl, log the interpolated value and check which segment injects the invalid part.

Example fix

// before
careers_url: getmanfred.com/api/feed
// after
careers_url: https://getmanfred.com/api/feed
Defensive patterns

Strategy: validation

Validate before calling

function isValidUrl(u) { try { new URL(u); return true; } catch { return false; } }
if (!isValidUrl(entry.careers_url)) throw new TypeError(`entry ${entry.name}: careers_url is not a valid absolute URL`);

Type guard

const isUrl = (v) => typeof v === 'string' && v.length > 0 && !Number.isNaN(Date.parse('x')) && (() => { try { new URL(v); return true; } catch { return false; } })();

Try / catch

try {
  await provider.fetch(entry, ctx);
} catch (err) {
  if (String(err.message).startsWith('manfred: invalid URL')) {
    console.error(`Bad careers_url for ${entry.name}: ${entry.careers_url}`);
    return;
  }
  throw err;
}

Prevention

When it happens

Trigger: 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.

Common situations: 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.

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


AI-assisted analysis of santifer/career-ops@e7abd431fc (2026-09-16). Data as JSON: /api/errors/72886175aca895f7. Report an issue: GitHub.

Appendix: source

Thrown at providers/manfred.mjs:41

const FEED_BASE = 'https://www.getmanfred.com/api/v2/public/offers';
// The full catalogue in one response runs ~2.3MB and takes 8-9.6s to arrive
// even on a clean connection — right against the shared 10s default timeout,
// so any network jitter aborts it. Give it real headroom rather than relying
// on retry alone to paper over a structurally near-timeout request.
const FETCH_TIMEOUT_MS = 25_000;
const TRUSTED_HOST = 'www.getmanfred.com';
const OFFER_BASE = 'https://www.getmanfred.com/ofertas-empleo';
const VALID_LANGS = ['EN', 'ES'];
const DEFAULT_LANG = 'EN';

/** @param {string} url */
function assertManfredUrl(url) {
  let parsed;
  try {
    parsed = new URL(url);
  } catch {
    throw new Error(`manfred: invalid URL: ${url}`);
  }
  if (parsed.protocol !== 'https:') throw new Error(`manfred: URL must use HTTPS: ${url}`);
  if (parsed.hostname !== TRUSTED_HOST) {
    throw new Error(`manfred: untrusted hostname "${parsed.hostname}" — must be ${TRUSTED_HOST}`);
  }
  return url;
}

/** Resolve the feed language: `lang` on the entry, uppercased, else EN. */
export function resolveLang(entry) {
  const raw = typeof entry?.lang === 'string' ? entry.lang.trim().toUpperCase() : '';
  return VALID_LANGS.includes(raw) ? raw : DEFAULT_LANG;
}

// The feed reports currency as the SYMBOL, not an ISO code, and the observed
// values include a narrow-no-break-space variant of the euro sign. scan.mjs's
// salary_filter compares currencies case-insensitively as plain strings, so a
// symbol would never match a user's `currency: EUR` — map to ISO, and drop the

View on GitHub (pinned to e7abd431fc)