headroomlabs-ai/headroom · error · Error
Headroom OpenCode wrap blocked direct HTTP/2 connection to $
Error message
Headroom OpenCode wrap blocked direct HTTP/2 connection to ${upstream.origin}. Use fetch, http, or https so traffic can be routed through Headroom. What it means
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.
Source
Thrown at plugins/opencode/src/transport.ts:408
} as HttpRequest | HttpsRequest;
}
function wrapGet(request: HttpRequest | HttpsRequest): HttpGet | HttpsGet {
return function headroomGet(this: unknown, ...args: unknown[]) {
const req = Reflect.apply(request, this, args);
req.end();
return req;
} as HttpGet | HttpsGet;
}
function wrapHttp2Connect(originalConnect: Http2Connect): Http2Connect {
return function headroomHttp2Connect(this: unknown, authority: string | URL, ...args: unknown[]) {
const state = getState();
if (state) {
const proxy = normalizeProxyUrl(state.proxyUrl);
const upstream = authority instanceof URL ? authority : new URL(String(authority));
if (shouldRoute(upstream, proxy)) {
throw new Error(
`Headroom OpenCode wrap blocked direct HTTP/2 connection to ${upstream.origin}. ` +
"Use fetch, http, or https so traffic can be routed through Headroom.",
);
}
}
return Reflect.apply(originalConnect, this, [authority, ...args]);
} as Http2Connect;
}
export function installHeadroomTransport(options: InstallOptions): () => void {
const existing = getState();
if (existing) {
existing.refs += 1;
existing.proxyUrl = options.proxyUrl;
existing.debug = Boolean(options.debug);
installProcessEnv(options.proxyUrl);
return () => uninstallHeadroomTransport();
}View on GitHub (pinned to 322425c43b)
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
Example fix
// before
import http2 from "node:http2";
const session = http2.connect("https://api.example.com"); // throws when routed
// after
const res = await fetch("https://api.example.com/v1/thing"); // wrapped, routed via Headroom Defensive patterns
Strategy: type-guard
Validate before calling
import http2 from "node:http2";
/** Detect the exact throw so callers can rewrite the call path. */
function isHttp2BlockedByHeadroom(e: unknown): boolean {
return e instanceof Error && e.message.includes("blocked direct HTTP/2 connection");
}
// Prefer APIs the transport can route, before reaching for http2:
// use fetch() / node:https for any origin the proxy routes. Type guard
import { isLocalProxyUrl } from "./url.js";
/** True when http2.connect to this origin will be passed through unwrapped. */
function http2Allowed(origin: string, proxyUrl: string): boolean {
// Same-origin as the proxy, local, or bypass-listed origins are not routed
if (origin === proxyUrl || isLocalProxyUrl(origin)) return true;
const noProxy = (process.env.NO_PROXY ?? "").split(",").map((s) => s.trim());
return noProxy.some((entry) => entry !== "" && origin.includes(entry));
} Try / catch
import http2 from "node:http2";
let session: ClientHttp2Session;
try {
session = http2.connect("https://api.example.com");
} catch (e) {
if (e instanceof Error && e.message.includes("blocked direct HTTP/2 connection")) {
// HTTP/2 cannot be proxied transparently — fall back to a routable API
const res = await fetch("https://api.example.com");
// handle res ...
} else {
throw e;
}
} Prevention
- 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
When it happens
Trigger: 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.
Common situations: 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.
Related errors
- Headroom OpenCode wrap blocked direct HTTP/2 connection to $
- Headroom OpenCode transport shim loaded without HEADROOM_OPE
- Headroom proxy startup is disabled
- failed to download {final_url} after {attempts} attempts: {e
- Error: Proxy dependencies not installed. Run: pip install he
AI-assisted analysis of headroomlabs-ai/headroom@322425c43b (2026-08-15).
Data as JSON: /api/errors/c5a7e7a21156b550.
Report an issue: GitHub.