{"record":{"id":"a330da9f06d58ec0","repo":"decolua/9router","slug":"headroom-url-must-use-http-or-https","errorCode":null,"errorMessage":"Headroom URL must use http or https","messagePattern":"Headroom URL must use http or https","errorType":"exception","errorClass":"Error","httpStatus":500,"severity":"error","filePath":"src/app/api/headroom/proxy/[...path]/route.js","lineNumber":26,"sourceCode":"  \"connection\",\n  \"keep-alive\",\n  \"proxy-authenticate\",\n  \"proxy-authorization\",\n  \"te\",\n  \"trailer\",\n  \"transfer-encoding\",\n  \"upgrade\",\n]);\n\nconst DASHBOARD_PREFIX = \"/api/headroom/proxy\";\nconst LOOPBACK_HOSTS = new Set([\"localhost\", \"127.0.0.1\", \"::1\"]);\n\nasync function getTargetBase() {\n  const settings = await getSettings();\n  const url = settings.headroomUrl || DEFAULT_HEADROOM_URL;\n  const target = new URL(url);\n  if (![\"http:\", \"https:\"].includes(target.protocol)) {\n    throw new Error(\"Headroom URL must use http or https\");\n  }\n  return target;\n}\n\nfunction buildTargetUrl(base, path, search) {\n  const target = new URL(base);\n  target.pathname = `/${path.join(\"/\")}`;\n  target.search = search;\n  return target;\n}\n\nfunction forwardedHeaders(request, target) {\n  const headers = new Headers(request.headers);\n  for (const header of headers.keys()) {\n    if (HOP_BY_HOP_HEADERS.has(header.toLowerCase())) headers.delete(header);\n  }\n  headers.delete(\"host\");\n  // Never leak viewer credentials to a non-loopback Headroom host","sourceCodeStart":8,"sourceCodeEnd":44,"githubUrl":"https://github.com/decolua/9router/blob/90b52e06ffd666b7929554211474d01588f6b1f8/src/app/api/headroom/proxy/[...path]/route.js#L8-L44","documentation":"getTargetBase resolves the Headroom upstream base URL from settings (headroomUrl or DEFAULT_HEADROOM_URL) and validates that its protocol is http: or https: before building a proxy target. Any other scheme makes the throw. This protects the catch-all proxy route from targets it cannot safely fetch.","triggerScenarios":"A request to /api/headroom/proxy/* when settings.headroomUrl is set to a URL with a non-http(s) scheme (e.g. 'ws://headroom:9930', 'file://...') or is otherwise malformed into an unexpected scheme by the URL parser.","commonSituations":"Operator stored a websocket or bare hostname (e.g. 'headroom.local:9000', which new URL() can mis-parse) in the headroom settings, or migrated config from another tool that used a different scheme.","solutions":["Open dashboard settings and set the Headroom URL to an http:// or https:// address, e.g. 'http://localhost:9000'.","Correct settings.headroomUrl in the persisted settings store if edited directly.","Ensure the URL includes the '//' after the scheme so it parses correctly."],"exampleFix":"// before\nheadroomUrl: \"ws://localhost:9000\"\n// after\nheadroomUrl: \"http://localhost:9000\"","handlingStrategy":"validation","validationCode":"const u = new URL(headroomUrl);\nif (![\"http:\", \"https:\"].includes(u.protocol)) {\n  throw new Error(\"headroomUrl must start with http:// or https://\");\n}","typeGuard":"const isHttpUrl = (v) => {\n  try { return [\"http:\", \"https:\"].includes(new URL(v).protocol); }\n  catch { return false; }\n};","tryCatchPattern":"try {\n  await fetchProxy(path);\n} catch (e) {\n  if (e.message === \"Headroom URL must use http or https\") {\n    console.error(\"Set headroomUrl to an http(s) URL in settings\");\n  } else throw e;\n}","preventionTips":["Store headroomUrl with a full scheme, e.g. http://localhost:9000","Avoid bare host:port values that parse with unexpected schemes","Validate settings after editing them via API or file"],"tags":["validation","url","proxy"],"backgroundTag":"invalid-url-scheme","analyzedSha":"90b52e06ffd666b7929554211474d01588f6b1f8","analyzedAt":"2026-08-30T21:05:45.952Z","schemaVersion":2},"datasetVersion":"2026-08-30T23:17:21.991Z"}