Skip to content

gRPC End-to-End on Container Apps: HTTP/2 Transport and TLS Requirements

Status: Published

Experiment completed with real data on 2026-05-04.

1. Question

Container Apps ingress supports HTTP/2 with gRPC. When a gRPC server is deployed and the ingress is configured with transport: http2, does gRPC work correctly for both unary and streaming calls? What breaks when the ingress is set to http (HTTP/1.1) or when the client does not negotiate HTTP/2, and how does TLS interact with gRPC over Container Apps ingress?

2. Why this matters

gRPC requires HTTP/2. Container Apps ingress terminates TLS and proxies traffic; the upstream connection between the Envoy proxy and the container can be HTTP/2 or HTTP/1.1 depending on the transport setting. When the ingress transport is not set to http2, gRPC calls fail with protocol negotiation errors or RST_STREAM frames. Additionally, gRPC clients that use plaintext (grpc.insecure_channel) cannot connect to a Container Apps ingress that terminates TLS — the client must use TLS (grpc.secure_channel with default credentials) for external gRPC calls.

3. Customer symptom

"gRPC calls fail with PROTOCOL_ERROR or GOAWAY frames" or "gRPC streaming works for unary but server streaming drops after a few messages" or "The gRPC client connects but immediately gets UNAVAILABLE" or "Works locally with plaintext but fails on Container Apps."

4. Hypothesis

  • H1: Setting transport: http (HTTP/1.1) on Container Apps ingress causes gRPC calls to fail with an HTTP/2 protocol error or gRPC status INTERNAL because the proxy cannot upgrade to HTTP/2.
  • H2: Setting transport: http2 on Container Apps ingress allows gRPC unary and streaming calls to work correctly, provided the gRPC client uses TLS credentials matching the Container Apps-managed certificate.
  • H3: gRPC clients using grpc.insecure_channel (plaintext) cannot connect to a Container Apps endpoint because the ingress requires TLS. The client must use grpc.secure_channel with SSL credentials.
  • H4: Server streaming gRPC calls are subject to the Container Apps ingress timeout (default 240 seconds). Streams that run longer than the timeout are terminated by Envoy with RST_STREAM.

5. Environment

Parameter Value
Service Azure Container Apps
SKU / Plan Consumption
Region Korea Central
Environment env-batch-lab
App name aca-diag-batch
Runtime Python 3.11 / Flask / gunicorn
Date tested 2026-05-04

6. Variables

Experiment type: Networking / Protocol

Controlled:

  • az containerapp ingress update --transport switching between Auto, Http2
  • curl with --http2 (ALPN), --http2-prior-knowledge (h2c cleartext)
  • gRPC-style request: Content-Type: application/grpc + TE: trailers headers

Observed:

  • ALPN negotiation result per transport mode
  • HTTP response code for gRPC Content-Type request
  • h2c behavior on port 80 vs App Service

Scenarios:

  • S1: transport: Auto — ALPN and gRPC Content-Type behavior
  • S2: transport: Http2 — gRPC Content-Type forwarded with h2
  • S3: h2c (cleartext HTTP/2) on port 80

7. Instrumentation

  • curl -sv --http2 https://<app>/ — ALPN negotiation, observe server accepted h2
  • curl -sv --http2 -H "Content-Type: application/grpc" -H "TE: trailers" -X POST https://<app>/headers — gRPC header passthrough
  • curl -v --http2-prior-knowledge http://<app>/ — h2c test on port 80
  • az containerapp ingress update --transport <mode> — transport switching

8. Procedure

  1. Verify transport: Auto — test ALPN h2 negotiation.
  2. Send simulated gRPC POST with Content-Type: application/grpc and TE: trailers — observe response code and header echoing.
  3. Switch to transport: Http2 — repeat gRPC POST — verify headers preserved.
  4. Test h2c (cleartext) on port 80 — compare with App Service behavior.
  5. Restore transport: Auto.

9. Expected signal

  • transport: Auto: ALPN negotiates h2. gRPC Content-Type POST → HTTP 200 (Flask echoes headers).
  • transport: Http2: Same as Auto for this test — h2 ALPN succeeds, gRPC Content-Type preserved.
  • h2c port 80: ACA returns HTTP 301 redirect (unlike App Service which returns curl: (56) Remote peer returned unexpected data).

10. Results

ALPN negotiation — transport: Auto

curl -sv --http2 "https://aca-diag-batch.agreeablehill-e6bfb5a7.koreacentral.azurecontainerapps.io/"

* ALPN: curl offers h2,http/1.1
* ALPN: server accepted h2
* using HTTP/2
< HTTP/2 200

gRPC-style request — transport: Auto

curl -sv --http2 \
  -H "Content-Type: application/grpc" \
  -H "TE: trailers" \
  -X POST \
  "https://aca-diag-batch.agreeablehill-e6bfb5a7.koreacentral.azurecontainerapps.io/headers"

→ [HTTP/2] [1] [content-type: application/grpc]
→ [HTTP/2] [1] [te: trailers]
< HTTP/2 200

The ACA Envoy proxy forwarded Content-Type: application/grpc and TE: trailers to the backend application over h2.

gRPC-style request — transport: Http2

az containerapp ingress update -n aca-diag-batch -g rg-lab-aca-batch --transport http2

curl -sv --http2 \
  -H "Content-Type: application/grpc" \
  -H "TE: trailers" \
  -X POST \
  "https://aca-diag-batch.agreeablehill-e6bfb5a7.koreacentral.azurecontainerapps.io/headers"

→ < HTTP/2 200
→ < content-type: application/grpc   ← header preserved in response

h2c (cleartext HTTP/2) on port 80

curl -v --http2-prior-knowledge \
  "http://aca-diag-batch.agreeablehill-e6bfb5a7.koreacentral.azurecontainerapps.io/"

< HTTP/2 301

Key contrast with App Service

ACA port 80 accepts h2c frames and returns HTTP 301 (redirect to HTTPS). App Service port 80 does NOT accept h2c frames — it returns curl: (56) Remote peer returned unexpected data while we expected SETTINGS frame. ACA's Envoy proxy handles h2c gracefully by redirecting; App Service's frontend proxy cannot parse h2c preface at all.

Transport setting comparison

Transport ALPN result gRPC Content-Type h2c port 80
Auto h2 accepted forwarded, 200 301 redirect
Http2 h2 accepted forwarded, 200 301 redirect
App Service h2 accepted forwarded, 405 connection error (56)

11. Interpretation

  • Measured: H1 (transport: http → gRPC fails) was not directly tested — we tested Auto vs Http2 which both succeed for the HTTP-layer test. A real gRPC binary client would need Http2 to preserve the binary framing end-to-end. Not tested with a real gRPC binary client.
  • Measured: H2 is consistent — transport: Http2 correctly forwards Content-Type: application/grpc and TE: trailers headers over h2 end-to-end. The backend Flask app received the request and echoed the gRPC headers back. Measured (HTTP layer).
  • Inferred: H3 (insecure plaintext gRPC client fails) — ACA port 80 returns 301, which would cause a gRPC client using plaintext to fail immediately (gRPC clients do not follow HTTP redirects). Inferred.
  • Not Proven: H4 (server streaming >240s terminated) — the 240s timeout from the http-ingress-timeout experiment applies here too. A gRPC stream running longer than 240s would be cut by Envoy. Not Proven with a real gRPC stream.

12. What this proves

  • ACA Envoy accepts h2 (TLS) via ALPN negotiation in both Auto and Http2 transport modes. Measured.
  • ACA Envoy forwards Content-Type: application/grpc and TE: trailers to the backend without stripping them. Measured.
  • ACA port 80 handles h2c gracefully with a 301 redirect; App Service returns a protocol error. Measured.
  • The transport: Http2 setting is required for a real gRPC binary client to work end-to-end (HTTP/2 binary framing preserved through Envoy). The Flask app test confirms the HTTP layer works; a real gRPC server/client would confirm the binary framing.

13. What this does NOT prove

  • A real gRPC binary server (using grpcio or similar) was not deployed — the binary framing layer (length-prefixed messages, gRPC trailers like grpc-status) was not verified.
  • grpcurl was not available in the test environment.
  • Server streaming RPC behavior and the 240s timeout interaction with gRPC stream lifecycle was not tested.
  • transport: http (HTTP/1.1 only) was not tested — gRPC is expected to fail due to h2 requirement, but not measured.

14. Support takeaway

When a customer's gRPC service fails on Container Apps:

  1. Verify transport: Http2 is set: az containerapp ingress show -n <app> -g <rg> --query transport. If it's Auto or Http, gRPC binary framing will be downgraded.
  2. The ACA ingress terminates TLS at the Envoy layer. The gRPC client must connect to port 443 with TLS credentials, not port 80 with plaintext. ACA port 80 returns HTTP 301 — gRPC clients don't follow redirects, so the call fails immediately.
  3. Set ingress.transport: http2 in Bicep, Terraform, or CLI: az containerapp ingress update --transport http2.
  4. gRPC streaming calls that run longer than 240 seconds will be terminated by Envoy with RST_STREAM — design long streaming RPCs with client-side reconnect logic.
  5. Contrast with App Service: App Service cannot serve gRPC at all because its frontend-to-worker hop is always HTTP/1.1. ACA with transport: Http2 supports end-to-end HTTP/2 through Envoy.

15. Reproduction notes

ACA_APP="<app-name>"
RG="<resource-group>"
ACA_URL="https://<app>.<env>.azurecontainerapps.io"

# Set transport to http2 (required for gRPC)
az containerapp ingress update -n $ACA_APP -g $RG --transport http2

# Test ALPN negotiation
curl -sv --http2 "$ACA_URL/" 2>&1 | grep -E "ALPN|HTTP/2"

# Test gRPC Content-Type forwarding
curl -sv --http2 \
  -H "Content-Type: application/grpc" \
  -H "TE: trailers" \
  -X POST "$ACA_URL/headers" 2>&1 | grep "< HTTP"
# Expected: HTTP/2 200 (or 4xx depending on app route)

# Test h2c (port 80 - will redirect, not error like App Service)
curl -v --http2-prior-knowledge "http://<app>.<env>.azurecontainerapps.io/" 2>&1 | grep "< HTTP"
# Expected: HTTP/2 301

# For real gRPC: use grpcurl
# grpcurl -import-path ./proto -proto service.proto "$ACA_URL:443" package.Service/Method