CORS Preflight Handling at Container Apps Ingress vs. Application Level¶
Status: Published
Experiment completed with real data on 2026-05-04.
1. Question¶
When a browser sends a CORS preflight request (HTTP OPTIONS with Origin and Access-Control-Request-* headers), is the preflight handled at the Container Apps ingress level, or forwarded to the application? When CORS is enabled on the ingress, what response headers are returned for allowed vs. disallowed origins?
2. Why this matters¶
CORS misconfigurations are a frequent source of support tickets. On Container Apps, unlike some platforms where the application handles CORS, ACA ingress supports platform-level CORS configuration. When CORS is not configured at the ingress, preflight OPTIONS requests return HTTP 200 with no Access-Control-* headers, causing browsers to block the cross-origin request. When wildcard * is enabled, all origins are allowed regardless of security intent. Understanding where CORS is enforced and what the exact response headers are prevents misconfiguration.
3. Customer symptom¶
"API calls work in Postman but fail from the browser with 'CORS error'" or "OPTIONS requests return 200 but no CORS headers" or "We enabled CORS but disallowed origins still get a 200 response."
4. Hypothesis¶
- H1: Without any CORS configuration on ACA ingress, OPTIONS preflight requests return HTTP 200 with no
Access-Control-*response headers. ✅ Confirmed - H2: After enabling CORS with a specific origin, OPTIONS requests from that origin receive
Access-Control-Allow-Origin,Access-Control-Allow-Methods, andAccess-Control-Allow-Headersheaders. ✅ Confirmed - H3: OPTIONS requests from a disallowed origin still receive HTTP 200 but without
Access-Control-Allow-Originin the response. ✅ Confirmed - H4: Enabling wildcard CORS (
allowedOrigins: ["*"]) causes all origins to receive the exactOriginvalue back inAccess-Control-Allow-Origin. ✅ Confirmed
5. Environment¶
| Parameter | Value |
|---|---|
| Service | Azure Container Apps |
| Environment | env-batch-lab (Consumption, Korea Central) |
| App | aca-diag-batch |
| Image | mcr.microsoft.com/azuredocs/containerapps-helloworld:latest |
| Ingress | External, transport: Auto, port 80 |
| Date tested | 2026-05-04 |
6. Variables¶
Experiment type: Networking / Configuration
Controlled:
- ACA app with external HTTPS ingress
- CORS configuration toggled via
az containerapp ingress cors enable
Observed:
- HTTP response code and
Access-Control-*headers for OPTIONS preflight - Behavior for allowed origin, disallowed origin, and wildcard configuration
Scenarios:
- S1: No CORS config → OPTIONS preflight from
https://example.com - S2: CORS enabled for
https://example.comonly → preflight from allowed and disallowed origins - S3: Wildcard CORS (
*) → preflight from any origin
7. Instrumentation¶
curl -sI -X OPTIONS -H "Origin: <origin>" -H "Access-Control-Request-Method: POST"— capture response headersaz containerapp ingress cors show— verify CORS configurationaz containerapp ingress cors enable— apply CORS settings
8. Procedure¶
- Created
aca-diag-batchwith no CORS configuration. - S1: Sent OPTIONS preflight from
https://example.com— captured headers. - S2: Enabled CORS for
https://example.comonly (--allowed-origins https://example.com); tested both allowed and disallowed origins. - S3: Changed CORS to wildcard (
--allowed-origins "*"); tested fromhttps://evil.com.
9. Expected signal¶
- S1: HTTP 200, no
Access-Control-*headers. - S2 (allowed): HTTP 200,
Access-Control-Allow-Origin: https://example.com, allowed methods and headers. - S2 (disallowed): HTTP 200, no
Access-Control-Allow-Origin. - S3: HTTP 200,
Access-Control-Allow-Origin: https://evil.com(echoes origin back).
10. Results¶
S1: No CORS configured:
```text OPTIONS / HTTP/2 200 (no Access-Control-* headers)
```text S2: CORS enabled for https://example.com only:
```text
Allowed origin (https://example.com):¶
HTTP/2 200 access-control-allow-origin: https://example.com access-control-allow-methods: GET,POST,OPTIONS access-control-allow-headers: Content-Type,Authorization
Disallowed origin (https://evil.com):¶
HTTP/2 200 (no Access-Control-* headers)
```text S3: Wildcard CORS (allowedOrigins: ["*"]):
```text
Request from https://evil.com:¶
HTTP/2 200 access-control-allow-origin: https://evil.com access-control-allow-methods: GET,POST,PUT,DELETE,OPTIONS access-control-allow-headers: (empty)
``text
**CORS configuration API (az containerapp ingress cors show`):**
```json { "allowCredentials": false, "allowedHeaders": [""], "allowedMethods": ["GET,POST,PUT,DELETE,OPTIONS"], "allowedOrigins": [""] }
```text
11. Interpretation¶
- Observed: Without CORS configuration, ACA ingress returns HTTP 200 to OPTIONS preflights but with no
Access-Control-*headers. The browser will block the actual request. The underlying container never receives the OPTIONS request — the ingress handles it. - Observed: With a specific origin configured, only that origin's preflight receives the
Access-Control-Allow-Originheader. Requests from other origins get HTTP 200 but no CORS headers — the request still fails in the browser, but the server doesn't return a 403. - Observed: Wildcard CORS (
*) causes the ingress to echo the requesting origin back inAccess-Control-Allow-Origin. This meanshttps://evil.comreceivesAccess-Control-Allow-Origin: https://evil.com— effectively allowing all origins including potentially malicious ones. - Observed: The
allowedHeaders: ["*"]in wildcard mode returns an emptyAccess-Control-Allow-Headersresponse header. This may cause preflight failures in strict browsers if specific headers are requested. - Inferred: The ACA ingress (Envoy-based) handles CORS at the proxy layer, not the application. The application code does not receive OPTIONS preflight requests when ingress-level CORS is configured.
12. What this proves¶
- ACA ingress handles CORS at the platform level — OPTIONS preflights do not reach the application container.
- Without CORS config: OPTIONS returns 200 with no CORS headers (browser blocks cross-origin requests).
- With specific origin configured: only matching origins receive CORS headers.
- Wildcard
*CORS reflects the requesting origin — all origins are effectively allowed. - Disallowed origins receive HTTP 200 but no
Access-Control-Allow-Origin— the browser enforces the block, not the server.
13. What this does NOT prove¶
- Application-level CORS middleware behavior (when the app handles OPTIONS itself) was Not Tested — requires a custom container image.
allowCredentials: trueinteraction with wildcard origins was Not Tested (CORS spec forbids this combination).Access-Control-Max-Agepreflight caching was Not Tested.- The behavior when ACA ingress CORS is disabled and the application has its own CORS middleware was Not Tested.
14. Support takeaway¶
- "CORS error in browser, OPTIONS returns 200" — check that
Access-Control-Allow-Originis actually present in the response. HTTP 200 on an OPTIONS preflight does not guarantee CORS headers are set. - "All origins can access my API" — check for wildcard CORS (
allowedOrigins: ["*"]). Remove wildcard and list specific allowed origins for security-sensitive APIs. - To add CORS at ingress:
az containerapp ingress cors enable -n <app> -g <rg> --allowed-origins "https://myapp.com". - CORS at ingress and CORS in the application code can conflict. If both set
Access-Control-Allow-Origin, the response may include duplicate headers, causing browser errors. Prefer one layer only.
15. Reproduction notes¶
```bash
Enable CORS for specific origin¶
az containerapp ingress cors enable \
-n
Test OPTIONS preflight¶
curl -sI -X OPTIONS \
-H "Origin: https://example.com" \
-H "Access-Control-Request-Method: POST" \
https://
Test disallowed origin¶
curl -sI -X OPTIONS \
-H "Origin: https://evil.com" \
-H "Access-Control-Request-Method: POST" \
https://
Returns 200 but NO Access-Control-Allow-Origin header¶
Check current CORS config¶
az containerapp ingress cors show -n
```text