{"record":{"id":"53415f1ca5a8e428","repo":"santifer/career-ops","slug":"h1b-api-base-must-not-contain-a-query-string-or-a","errorCode":null,"errorMessage":"H1B_API_BASE must not contain a query string or a fragment.","messagePattern":"H1B_API_BASE must not contain a query string or a fragment\\.","errorType":"validation","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"plugins/h1b-sponsor/lib/api.mjs","lineNumber":60,"sourceCode":"  if (!trimmed) {\n    throw new Error('H1B_API_BASE is set but empty. Unset it to use the default endpoint.');\n  }\n\n  let parsed;\n  try {\n    parsed = new URL(trimmed);\n  } catch {\n    throw new Error(`H1B_API_BASE is not a valid URL: ${trimmed}`);\n  }\n  if (parsed.username || parsed.password) {\n    // Undici refuses a credentialed Request anyway, and the value reaches\n    // stdout through the source field, so this would print a password.\n    throw new Error('H1B_API_BASE must not embed credentials.');\n  }\n  if (parsed.search || parsed.hash) {\n    // Paths are appended, so a query or fragment swallows them: the request\n    // would go to the base itself and answer about a company never asked for.\n    throw new Error('H1B_API_BASE must not contain a query string or a fragment.');\n  }\n  // Plain http would put an Authorization header on the wire in the clear.\n  // Loopback is exempt so a self-hoster can develop against a local worker,\n  // but only for http. Exempting every scheme on a loopback host let\n  // ftp://localhost and ws://localhost past validation, and those die later\n  // inside fetch as a bare \"fetch failed\", which is the opaque failure this\n  // check exists to replace with a named configuration error.\n  const loopback = /^(localhost|127\\.\\d+\\.\\d+\\.\\d+|\\[::1\\]|::1)$/i.test(parsed.hostname);\n  const allowedScheme = parsed.protocol === 'https:' || (parsed.protocol === 'http:' && loopback);\n  if (!allowedScheme) {\n    throw new Error(`H1B_API_BASE must use https, or http on loopback: ${trimmed}`);\n  }\n  // Appended as `${base}/employers/...`, so a trailing slash would double up.\n  return trimmed.replace(/\\/+$/, '');\n}\nconst USER_AGENT = 'career-ops-plugin-h1b-sponsor/1.0';\nconst DEFAULT_TIMEOUT_MS = 10_000;\nconst MAX_RETRY_WAIT_MS = 10_000;","sourceCodeStart":42,"sourceCodeEnd":78,"githubUrl":"https://github.com/santifer/career-ops/blob/1696bec4d021768e7359f9aad6b329cba883da20/plugins/h1b-sponsor/lib/api.mjs#L42-L78","documentation":"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.","triggerScenarios":"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/...`.","commonSituations":"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.","solutions":["Strip everything from the '?' onward: keep only scheme, host, and path prefix in H1B_API_BASE.","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.","Verify with node: `const u = new URL(v); u.search || u.hash` must both be empty."],"exampleFix":"// before (.env)\nH1B_API_BASE=https://api.example.com/v1?apikey=xyz\n// after\nH1B_API_BASE=https://api.example.com/v1","handlingStrategy":"validation","validationCode":"function baseHasQueryOrFragment(v) {\n  try { const u = new URL(v); return Boolean(u.search || u.hash); } catch { return false; }\n}\n// guard: if (baseHasQueryOrFragment(process.env.H1B_API_BASE)) strip or fix it;","typeGuard":"function isCleanBaseUrl(v) {\n  try { const u = new URL(v); return !u.search && !u.hash; } catch { return false; }\n}","tryCatchPattern":"try {\n  const data = await lookupEmployer(name);\n} catch (e) {\n  if (e.message.includes('must not contain a query string or a fragment')) {\n    console.error('H1B_API_BASE must be scheme+host+path only; move any api key out of the URL.');\n    return;\n  }\n  throw e;\n}","preventionTips":["Strip tracking params and #anchors when copying a URL into config.","Remember the base is a prefix: paths like /employers/... are appended, so '?' or '#' would swallow them.","Normalize the value once at startup: new URL(v) then rebuild without search/hash (or reject).","Never try to pass API keys as query params in the base URL."],"tags":["configuration","url-validation","env-var"],"backgroundTag":"invalid-base-url","analyzedSha":"1696bec4d021768e7359f9aad6b329cba883da20","analyzedAt":"2026-09-01T19:19:23.111Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-08T10:18:20.063Z"}