cloudflare / cloudflare/containers
Runtime per-host outbound overrides with interceptHttps intercept all HTTPS, breaking HTTP/2/gRPC egress
- Dominant language
- TypeScript
- Stars
- 270
- Forks
- 42
- Avg merge
- 1d 5h
- Merged PRs (30d)
- 4
Description
### Summary
Using `setOutboundByHosts(...)` with `interceptHttps = true` appears to promote a Container from per-host outbound interception to global HTTPS interception. As a result, unrelated HTTPS egress from inside the container is routed through `ContainerProxy` / Worker `fetch`, which is not transparent HTTP/2/gRPC transport and breaks gRPC clients that require ALPN `h2` and HTTP/2 trailers.
This is surprising because the application only intends to intercept specific hosts for credential injection, while allowing normal public internet egress for everything else.
### Environment
- Package: `@cloudflare/containers@0.3.3`
- Wrangler: `4.86.0`
- Worker compatibility date: `2026-04-18`
- Container DO:
- `enableInternet = true`
- `interceptHttps = true`
- runtime host overrides configured with `setOutboundByHosts(...)`
### Minimal shape of the setup
```ts
import { Container } from "@cloudflare/containers";
export class MyContainer extends Container {
defaultPort = 7070;
enableInternet = true;
interceptHttps = true;
}
MyContainer.outboundHandlers = {
injectAuth(req: Request, env: Env, ctx) {
const headers = new Headers(req.headers);
headers.set("authorization", `Bearer ${ctx.params.token}`);
return fetch(new Request(req, { headers }));
},
};
class MyContainerDO extends MyContainer {
async init() {
await this.setOutboundByHosts({
"api.example.com": {
method: "injectAuth",
params: { token: "..." },
},
});
}
}
```
### Expected behavior
Only `api.example.com` HTTPS traffic should be intercepted.
Other HTTPS traffic from the container, including gRPC/HTTP2 connections to unrelated hosts, should egress normally when `enableInternet = true`.
Alternatively, if this is intentionally unsupported, the docs/API should make clear that runtime per-host overrides plus `interceptHttps = true` cause global HTTPS interception and are not suitable for HTTP/2/gRPC egress.
### Actual behavior
After configuring runtime per-host overrides, unrelated HTTPS traffic appears to be TLS-intercepted and routed through the Worker outbound handler path.
In `@cloudflare/containers@0.3.3`, this seems to come from:
```js
hasMutableOutboundConfiguration() {
return (
Object.keys(this.outboundByHostOverrides).length > 0 ||
this.allowedHostsOverride !== undefined ||
this.deniedHostsOverride !== undefined
);
}
shouldInterceptAllOutbound() {
return (
this.hasInterceptAllRegistration ||
this.needsCatchAllInterception() ||
this.effectiveAllowedHosts !== undefined ||
this.effectiveDeniedHosts !== undefined ||
this.hasMutableOutboundConfiguration()
);
}
```
Then `applyOutboundInterception()` does:
```js
if (interceptAll) {
if (this.interceptHttps) {
await this.container.interceptOutboundHttps("*", fetcher);
}
await this.container.interceptAllOutboundHttp(fetcher);
}
```
So a runtime per-host config causes `interceptOutboundHttps("*", ...)`.
### Why this breaks gRPC / HTTP2
gRPC requires HTTP/2 over TLS, ALPN `h2`, and HTTP/2 trailers such as `grpc-status`.
The outbound interception path hands traffic to a Worker `Request` handler / `fetch()` path. That is not a transparent HTTP/2 tunnel. Workers' HTTP compatibility docs also note that trailer headers are not supported in the Node HTTP implementation, and the Fetch API does not expose raw HTTP/2 framing or ALPN behavior.
Typical symptoms from inside the container:
```sh
grpcurl -vv grpc-host.example.com:443 list
# fails with transport / protocol / unavailable errors
openssl s_client -alpn h2 -connect grpc-host.example.com:443 -servername grpc-host.example.com
# shows the Cloudflare Containers interception CA and/or does not negotiate the expected h2 path
```
The same gRPC target works when HTTPS interception is disabled or when the runtime outbound overrides are not configured.
### Impact
This makes it difficult to safely combine:
- per-instance credential injection for a few HTTPS APIs, and
- normal container egress for services that require native HTTP/2/gRPC.
The current behavior also has a broad blast radius: adding a runtime host override for one API changes transport behavior for all HTTPS destinations.
### Workarounds
Known workaround is to avoid HTTPS interception for this use case:
- set `interceptHttps = false`
- use plain HTTP synthetic/internal hostnames for calls that intentionally route to Worker handlers
- have the Worker handler upgrade those requests to HTTPS when forwarding to the real upstream
That works for credential injection, but it means HTTPS host interception cannot be used safely in containers that also need arbitrary gRPC/HTTP2 egress.
### Requested fix
Any of these would help:
1. Keep `setOutboundByHosts(...)` as true per-host interception instead of promoting to `interceptOutboundHttps("*", ...)`.
2. Add an explicit passthrough / exclusion API for hosts that must not be TLS-intercepted.
3. Document that `interceptHttps` outbound handlers are HTTP-level interception only and are not compatible with native gRPC/HTTP2 traffic.
4. Provide a supported pattern for credential-injection handlers that does not globally intercept unrelated HTTPS egress.
### Related docs
- Containers outbound interception: https://developers.cloudflare.com/containers/platform-details/outbound-traffic/
- Cloudflare gRPC requirements: https://developers.cloudflare.com/network/grpc-connections/
- Workers protocol support: https://developers.cloudflare.com/workers/reference/protocols/
- Workers Node HTTP limitations: https://developers.cloudflare.com/workers/runtime-apis/nodejs/http/
Contributor guide
Assessment
This issue has not been assessed yet.