{"record":{"id":"0d5883c88037c9bd","repo":"santifer/career-ops","slug":"h1b-api-base-must-use-https-or-http-on-loopback","errorCode":null,"errorMessage":"H1B_API_BASE must use https, or http on loopback: ${trimmed}","messagePattern":"H1B_API_BASE must use https, or http on loopback: (.+?)","errorType":"validation","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"plugins/h1b-sponsor/lib/api.mjs","lineNumber":71,"sourceCode":"    // 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;\nconst MAX_REDIRECTS = 3;\n// The largest real employer profile this API serves is about 13 KB and a search\n// page is about 4 KB, so a megabyte is ~80x headroom for future growth while\n// still refusing to buffer a runaway or hostile body.\nconst MAX_READ_BYTES = 1024 * 1024;\n\nfunction buildHeaders(token) {\n  const h = { 'Accept': 'application/json', 'User-Agent': USER_AGENT };\n  if (typeof token === 'string' && token.trim()) {\n    h['Authorization'] = `Bearer ${token.trim()}`;\n  }","sourceCodeStart":53,"sourceCodeEnd":89,"githubUrl":"https://github.com/santifer/career-ops/blob/1696bec4d021768e7359f9aad6b329cba883da20/plugins/h1b-sponsor/lib/api.mjs#L53-L89","documentation":"resolveBase() enforces a transport-safety policy: H1B_API_BASE must be https, except plain http is allowed on loopback hosts (localhost, 127.x, ::1) so a self-hoster can develop against a local worker. Exempting every scheme on loopback previously let ftp://localhost and ws://localhost through, which later died as an opaque 'fetch failed'.","triggerScenarios":"Setting H1B_API_BASE to an http:// URL with a non-loopback hostname (e.g. http://api.example.com), or to a non-http(s) scheme entirely (ftp:, ws:), then calling any plugin API function.","commonSituations":"Pointing at an internal staging server that lacks TLS; accidentally leaving 'http://' from a local dev config when switching to production; using ws:// or ftp:// because the docs snippet was copied from another service.","solutions":["Serve the API over https and set H1B_API_BASE to the https:// URL.","For local development, use http://localhost:PORT or http://127.0.0.1:PORT — loopback http is explicitly allowed.","Put TLS in front of a plain-http internal server (reverse proxy with a certificate) instead of relaxing the scheme.","Do not use ws://, ftp://, or other schemes — fetch only speaks http(s)."],"exampleFix":"// before (.env)\nH1B_API_BASE=http://api.staging.example.com\n// after\nH1B_API_BASE=https://api.staging.example.com\n// or, for local dev:\n// H1B_API_BASE=http://localhost:8787","handlingStrategy":"validation","validationCode":"const LOOPBACK = /^(localhost|127\\.\\d+\\.\\d+\\.\\d+|\\[::1\\]|::1)$/i;\nfunction isAllowedScheme(v) {\n  try {\n    const u = new URL(v);\n    return u.protocol === 'https:' || (u.protocol === 'http:' && LOOPBACK.test(u.hostname));\n  } catch { return false; }\n}\n// if (!isAllowedScheme(process.env.H1B_API_BASE)) fix before running;","typeGuard":"function isSecureOrLoopback(v) {\n  try {\n    const u = new URL(v);\n    return u.protocol === 'https:' ||\n      (u.protocol === 'http:' && /^(localhost|127\\.|\\[::1\\]|::1)/i.test(u.hostname));\n  } catch { return false; }\n}","tryCatchPattern":"try {\n  await lookupEmployer(name);\n} catch (e) {\n  if (e.message.startsWith('H1B_API_BASE must use https')) {\n    console.error('Use https:// in production; plain http only for localhost/127.x dev.');\n    return;\n  }\n  throw e;\n}","preventionTips":["Default to https everywhere; only drop to http for loopback development.","Keep separate .env.development (http://localhost:PORT) and .env.production (https://host) files.","Never use non-http(s) schemes (ws:, ftp:) — fetch cannot speak them.","Put TLS on internal servers via a reverse proxy instead of downgrading the scheme."],"tags":["configuration","tls","url-validation"],"backgroundTag":"insecure-scheme","analyzedSha":"1696bec4d021768e7359f9aad6b329cba883da20","analyzedAt":"2026-09-01T19:19:23.111Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}