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 statusINTERNALbecause the proxy cannot upgrade to HTTP/2. - H2: Setting
transport: http2on 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 usegrpc.secure_channelwith 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 --transportswitching betweenAuto,Http2- curl with
--http2(ALPN),--http2-prior-knowledge(h2c cleartext) - gRPC-style request:
Content-Type: application/grpc+TE: trailersheaders
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, observeserver accepted h2curl -sv --http2 -H "Content-Type: application/grpc" -H "TE: trailers" -X POST https://<app>/headers— gRPC header passthroughcurl -v --http2-prior-knowledge http://<app>/— h2c test on port 80az containerapp ingress update --transport <mode>— transport switching
8. Procedure¶
- Verify
transport: Auto— test ALPN h2 negotiation. - Send simulated gRPC POST with
Content-Type: application/grpcandTE: trailers— observe response code and header echoing. - Switch to
transport: Http2— repeat gRPC POST — verify headers preserved. - Test h2c (cleartext) on port 80 — compare with App Service behavior.
- 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
AutovsHttp2which both succeed for the HTTP-layer test. A real gRPC binary client would needHttp2to preserve the binary framing end-to-end. Not tested with a real gRPC binary client. - Measured: H2 is consistent —
transport: Http2correctly forwardsContent-Type: application/grpcandTE: trailersheaders 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
AutoandHttp2transport modes. Measured. - ACA Envoy forwards
Content-Type: application/grpcandTE: trailersto 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: Http2setting 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
grpcioor similar) was not deployed — the binary framing layer (length-prefixed messages, gRPC trailers likegrpc-status) was not verified. grpcurlwas 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:
- Verify
transport: Http2is set:az containerapp ingress show -n <app> -g <rg> --query transport. If it'sAutoorHttp, gRPC binary framing will be downgraded. - 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.
- Set
ingress.transport: http2in Bicep, Terraform, or CLI:az containerapp ingress update --transport http2. - 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.
- 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: Http2supports 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