{"record":{"id":"c5a7e7a21156b550","repo":"headroomlabs-ai/headroom","slug":"headroom-opencode-wrap-blocked-direct-http-2-conne-c5a7e7","errorCode":null,"errorMessage":"Headroom OpenCode wrap blocked direct HTTP/2 connection to ${upstream.origin}. Use fetch, http, or https so traffic can be routed through Headroom.","messagePattern":"Headroom OpenCode wrap blocked direct HTTP/2 connection to (.+?)\\. Use fetch, http, or https so traffic can be routed through Headroom\\.","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"plugins/opencode/src/transport.ts","lineNumber":408,"sourceCode":"  } as HttpRequest | HttpsRequest;\n}\n\nfunction wrapGet(request: HttpRequest | HttpsRequest): HttpGet | HttpsGet {\n  return function headroomGet(this: unknown, ...args: unknown[]) {\n    const req = Reflect.apply(request, this, args);\n    req.end();\n    return req;\n  } as HttpGet | HttpsGet;\n}\n\nfunction wrapHttp2Connect(originalConnect: Http2Connect): Http2Connect {\n  return function headroomHttp2Connect(this: unknown, authority: string | URL, ...args: unknown[]) {\n    const state = getState();\n    if (state) {\n      const proxy = normalizeProxyUrl(state.proxyUrl);\n      const upstream = authority instanceof URL ? authority : new URL(String(authority));\n      if (shouldRoute(upstream, proxy)) {\n        throw new Error(\n          `Headroom OpenCode wrap blocked direct HTTP/2 connection to ${upstream.origin}. ` +\n            \"Use fetch, http, or https so traffic can be routed through Headroom.\",\n        );\n      }\n    }\n    return Reflect.apply(originalConnect, this, [authority, ...args]);\n  } as Http2Connect;\n}\n\nexport function installHeadroomTransport(options: InstallOptions): () => void {\n  const existing = getState();\n  if (existing) {\n    existing.refs += 1;\n    existing.proxyUrl = options.proxyUrl;\n    existing.debug = Boolean(options.debug);\n    installProcessEnv(options.proxyUrl);\n    return () => uninstallHeadroomTransport();\n  }","sourceCodeStart":390,"sourceCodeEnd":426,"githubUrl":"https://github.com/headroomlabs-ai/headroom/blob/322425c43bffde1ed0b64fecf3cf5951565dd82b/plugins/opencode/src/transport.ts#L390-L426","documentation":"The transport wrapper monkey-patches Node's http2.connect: HTTP/2 connections cannot be transparently routed through an HTTP forward proxy the way http/https/fetch can, so when the target origin matches the routing rules (shouldRoute, given the configured proxy), the wrapper throws instead of letting traffic bypass Headroom. This is an intentional enforcement of 'all routed traffic goes through Headroom', not a routing bug.","triggerScenarios":"After installHeadroomTransport({ proxyUrl }), any code calls http2.connect('https://api.example.com') (or a library like gRPC does internally) for an origin that shouldRoute() matches — not excluded via proxy bypass rules and not the proxy itself.","commonSituations":"A dependency using node-http2 or native http2 for an API call that used to work; gRPC/protobuf clients in the same process; analytics SDKs preferring HTTP/2; the app expects direct egress but the transport is installed globally.","solutions":["Switch the call to fetch(), http.request, or https.request — these are wrapped and routed through Headroom correctly","If the origin must go direct, exclude it from routing (e.g. NO_PROXY / the bypass configuration consumed by shouldRoute) so http2.connect passes through unwrapped","In libraries, prefer undici/fetch-style APIs that the wrapper can intercept instead of raw h2 sessions"],"exampleFix":"// before\nimport http2 from \"node:http2\";\nconst session = http2.connect(\"https://api.example.com\"); // throws when routed\n\n// after\nconst res = await fetch(\"https://api.example.com/v1/thing\"); // wrapped, routed via Headroom","handlingStrategy":"type-guard","validationCode":"import http2 from \"node:http2\";\n\n/** Detect the exact throw so callers can rewrite the call path. */\nfunction isHttp2BlockedByHeadroom(e: unknown): boolean {\n  return e instanceof Error && e.message.includes(\"blocked direct HTTP/2 connection\");\n}\n\n// Prefer APIs the transport can route, before reaching for http2:\n// use fetch() / node:https for any origin the proxy routes.","typeGuard":"import { isLocalProxyUrl } from \"./url.js\";\n\n/** True when http2.connect to this origin will be passed through unwrapped. */\nfunction http2Allowed(origin: string, proxyUrl: string): boolean {\n  // Same-origin as the proxy, local, or bypass-listed origins are not routed\n  if (origin === proxyUrl || isLocalProxyUrl(origin)) return true;\n  const noProxy = (process.env.NO_PROXY ?? \"\").split(\",\").map((s) => s.trim());\n  return noProxy.some((entry) => entry !== \"\" && origin.includes(entry));\n}","tryCatchPattern":"import http2 from \"node:http2\";\n\nlet session: ClientHttp2Session;\ntry {\n  session = http2.connect(\"https://api.example.com\");\n} catch (e) {\n  if (e instanceof Error && e.message.includes(\"blocked direct HTTP/2 connection\")) {\n    // HTTP/2 cannot be proxied transparently — fall back to a routable API\n    const res = await fetch(\"https://api.example.com\");\n    // handle res ...\n  } else {\n    throw e;\n  }\n}","preventionTips":["Prefer fetch()/node:https over http2.connect in code that runs under the Headroom transport","Audit dependencies (gRPC clients, HTTP/2-preferring SDKs) for raw h2 usage before installing the transport","Add origins that legitimately need direct h2 to NO_PROXY / the routing bypass list"],"tags":["http2","transport","proxy","opencode","typescript"],"backgroundTag":null,"analyzedSha":"322425c43bffde1ed0b64fecf3cf5951565dd82b","analyzedAt":"2026-08-15T01:03:05.481Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}