santifer/career-ops · error · Error

pinpoint: invalid URL: ${url}

Error message

pinpoint: invalid URL: ${url}

What it means

Thrown by pinpoint's assertPinpointUrl() when new URL(url) throws — the URL is syntactically invalid. First of three SSRF gates (valid URL → HTTPS → trusted hostname regex) for Pinpoint ATS boards. The hostname regex PINPOINT_HOST_RE enforces the <slug>.pinpointhq.com pattern, requiring a slug that starts and ends with alphanumeric (allowing internal hyphens).

Source

Thrown at providers/pinpoint.mjs:30

//
// Per-tenant subdomains are the variable part — SSRF defence uses a regex
// match on `<safe-slug>.pinpointhq.com` rather than a static allowlist, the
// same approach as the recruitee provider.

// The tenant label must be a valid DNS label: it may contain hyphens but must
// not start or end with one (so `acme-.pinpointhq.com` is rejected). The
// optional trailing group keeps single-character labels (e.g. `a.pinpointhq.com`)
// valid. detect() and fetch() both route through this constant via
// resolveApiUrl()/assertPinpointUrl(), so the stricter check applies everywhere.
const PINPOINT_HOST_RE = /^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.pinpointhq\.com$/;

/** @param {string} url */
function assertPinpointUrl(url) {
  let parsed;
  try {
    parsed = new URL(url);
  } catch {
    throw new Error(`pinpoint: invalid URL: ${url}`);
  }
  if (parsed.protocol !== 'https:') throw new Error(`pinpoint: URL must use HTTPS: ${url}`);
  if (!PINPOINT_HOST_RE.test(parsed.hostname)) {
    throw new Error(`pinpoint: untrusted hostname "${parsed.hostname}" — must match <slug>.pinpointhq.com`);
  }
  return url;
}

function resolveApiUrl(entry) {
  const raw = typeof entry.careers_url === 'string' ? entry.careers_url : '';
  if (!raw) return null;
  let parsed;
  try {
    parsed = new URL(raw);
  } catch {
    return null;
  }
  if (parsed.protocol !== 'https:') return null;

View on GitHub (pinned to 9b17a8ac97)

Solutions

  1. Log the url argument to assertPinpointUrl to identify the malformed value.
  2. Set careers_url in the pinpoint portals.yml entry to https://<slug>.pinpointhq.com.
  3. If calling resolveApiUrl() directly, validate the entry has a non-empty careers_url first.

Example fix

// before — missing scheme
careers_url: 'acme.pinpointhq.com'

// after
careers_url: 'https://acme.pinpointhq.com'
Defensive patterns

Strategy: validation

Validate before calling

/** Validate URL string is parseable before passing to assertPinpointUrl. */
function isValidUrlString(url) {
  return typeof url === 'string'
    && url.length > 0
    && (() => { try { new URL(url); return true; } catch { return false; } })();
}

if (!isValidUrlString(entry.careers_url)) {
  console.warn(`pinpoint entry ${entry.name} has invalid URL`);
  continue;
}

Type guard

/** @param {unknown} url @returns {url is string} */
function isParseableUrl(url) {
  if (typeof url !== 'string' || !url) return false;
  try { new URL(url); return true; } catch { return false; }
}

Try / catch

try {
  await pinpointProvider.fetch(entry, ctx);
} catch (err) {
  if (String(err.message).startsWith('pinpoint: invalid URL')) {
    console.warn(`skipping pinpoint entry ${entry.name}: malformed URL`);
    continue;
  }
  throw err;
}

Prevention

When it happens

Trigger: Called with an unparseable URL: undefined, empty string, spaces, or a schemeless path. The guard is called from resolveApiUrl() and fetch(), both routing through assertPinpointUrl. A typical trigger is a careers_url with a typo or missing scheme that reaches assertPinpointUrl past detect()'s null-return guard.

Common situations: Portals.yml pinpoint entry with careers_url missing or malformed. A programmatic entry without the URL field. A URL copied without the https:// scheme. Testing with a relative fixture path.

Related errors


AI-assisted analysis of santifer/career-ops@9b17a8ac97 (2026-08-13). Data as JSON: /api/errors/e2d4580ad10a6ef4. Report an issue: GitHub.