Skip to content

Runtime per-host outbound overrides with interceptHttps intercept all HTTPS, breaking HTTP/2/gRPC egress #195

@neekolas

Description

@neekolas

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

import { Container } from "@cloudflare/containers";

export class MyContainer extends Container<Env> {
  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:

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:

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:

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions