santifer/career-ops · error · Error

H1B_API_BASE must not contain a query string or a fragment.

Error message

H1B_API_BASE must not contain a query string or a fragment.

What it means

resolveBase() rejects an H1B_API_BASE containing a query string (?) or fragment (#). Paths are appended to the base as `${base}/employers/...`, so a query or fragment would swallow the appended path — the request would hit the base URL itself and return data about the wrong (or no) company.

Source

Thrown at plugins/h1b-sponsor/lib/api.mjs:60

  if (!trimmed) {
    throw new Error('H1B_API_BASE is set but empty. Unset it to use the default endpoint.');
  }

  let parsed;
  try {
    parsed = new URL(trimmed);
  } catch {
    throw new Error(`H1B_API_BASE is not a valid URL: ${trimmed}`);
  }
  if (parsed.username || parsed.password) {
    // Undici refuses a credentialed Request anyway, and the value reaches
    // stdout through the source field, so this would print a password.
    throw new Error('H1B_API_BASE must not embed credentials.');
  }
  if (parsed.search || parsed.hash) {
    // Paths are appended, so a query or fragment swallows them: the request
    // would go to the base itself and answer about a company never asked for.
    throw new Error('H1B_API_BASE must not contain a query string or a fragment.');
  }
  // Plain http would put an Authorization header on the wire in the clear.
  // Loopback is exempt so a self-hoster can develop against a local worker,
  // but only for http. Exempting every scheme on a loopback host let
  // ftp://localhost and ws://localhost past validation, and those die later
  // inside fetch as a bare "fetch failed", which is the opaque failure this
  // check exists to replace with a named configuration error.
  const loopback = /^(localhost|127\.\d+\.\d+\.\d+|\[::1\]|::1)$/i.test(parsed.hostname);
  const allowedScheme = parsed.protocol === 'https:' || (parsed.protocol === 'http:' && loopback);
  if (!allowedScheme) {
    throw new Error(`H1B_API_BASE must use https, or http on loopback: ${trimmed}`);
  }
  // Appended as `${base}/employers/...`, so a trailing slash would double up.
  return trimmed.replace(/\/+$/, '');
}
const USER_AGENT = 'career-ops-plugin-h1b-sponsor/1.0';
const DEFAULT_TIMEOUT_MS = 10_000;
const MAX_RETRY_WAIT_MS = 10_000;

View on GitHub (pinned to 1696bec4d0)

Solutions

  1. Strip everything from the '?' onward: keep only scheme, host, and path prefix in H1B_API_BASE.
  2. If the API requires a key as a query parameter, use a proxy that appends it, or a base path that encodes it, not the base URL.
  3. Verify with node: `const u = new URL(v); u.search || u.hash` must both be empty.

Example fix

// before (.env)
H1B_API_BASE=https://api.example.com/v1?apikey=xyz
// after
H1B_API_BASE=https://api.example.com/v1
Defensive patterns

Strategy: validation

Validate before calling

function baseHasQueryOrFragment(v) {
  try { const u = new URL(v); return Boolean(u.search || u.hash); } catch { return false; }
}
// guard: if (baseHasQueryOrFragment(process.env.H1B_API_BASE)) strip or fix it;

Type guard

function isCleanBaseUrl(v) {
  try { const u = new URL(v); return !u.search && !u.hash; } catch { return false; }
}

Try / catch

try {
  const data = await lookupEmployer(name);
} catch (e) {
  if (e.message.includes('must not contain a query string or a fragment')) {
    console.error('H1B_API_BASE must be scheme+host+path only; move any api key out of the URL.');
    return;
  }
  throw e;
}

Prevention

When it happens

Trigger: Setting H1B_API_BASE to a URL like https://api.example.com?key=abc or https://example.com/page#section and calling any function that constructs `${apiBase()}/employers/...`.

Common situations: Pasting a full page URL from a browser (which carries ?utm_... or # anchors) into the config; trying to pass an API key as a query parameter in the base URL.

Related errors


AI-assisted analysis of santifer/career-ops@1696bec4d0 (2026-09-01). Data as JSON: /api/errors/53415f1ca5a8e428. Report an issue: GitHub.