ADR-056: Intelligent Multi-Environment CORS Configuration
Status: Accepted
Date: 2025-10-29
Deciders: Infrastructure Team, Backend Team
Related: ADR-049 (Private IP Networking), ADR-053 (Infrastructure Consolidation), ADR-054 (Application Gateway)
Context
CORS (Cross-Origin Resource Sharing) configuration consistently failed across deployment environments due to:
- Hardcoded localhost origins in parameter files (
http://localhost:3000) - Missing Application Gateway FQDN in allowed origins
- Static configuration requiring container redeployment for changes
- No environment awareness - same config used for dev/test/prod
- Silent failures - no logging of CORS rejections
Previous State (Broken)
# deploy-apps.sh
CORS_ORIGINS="http://localhost:3000" # Wrong port (UI is 8080)
# Missing: Application Gateway FQDN from Substrate outputs
Result: Every Azure deployment with Application Gateway resulted in CORS failures because:
- Browser loads UI from https://ldfdev8.eastus.cloudapp.azure.com/
- UI calls API at https://ldfdev8.eastus.cloudapp.azure.com/loan-defenders/api/
- API sees Origin: https://ldfdev8.eastus.cloudapp.azure.com
- API allows: ["http://localhost:3000"]
- CORS REJECTION ❌
Decision
Implement intelligent multi-environment CORS configuration with:
1. Smart Auto-Detection in API Config
# config.py
cors_origins: list[str] | str | None = None # Optional with smart defaults
@field_validator("cors_origins", mode="before")
@classmethod
def parse_cors_origins(cls, v: Any) -> list[str]:
if v is not None:
# Explicit config takes precedence
return [origin.strip() for origin in v.split(",")]
# Auto-detect deployment environment
origins = []
# Azure: Check for Application Gateway
if app_gateway_url := os.getenv("APP_GATEWAY_URL"):
origins.append(app_gateway_url)
origins.append(app_gateway_url.replace("http://", "https://"))
# Docker: Check for container environment
if os.getenv("DOCKER_CONTAINER") or os.path.exists("/.dockerenv"):
origins.extend([
"http://ui:8080",
"http://loan-defenders-ui:8080",
"http://localhost:8080",
])
# Always include common dev ports
origins.extend([
"http://localhost:3000",
"http://localhost:5173",
"http://localhost:8080",
])
return list(dict.fromkeys(origins)) # Deduplicate
2. Deployment Script Integration
# deploy-apps.sh
# 1. Retrieve Application Gateway FQDN from Key Vault
APP_GATEWAY_FQDN=$(retrieve_output_robust "appGatewayPublicFqdn" "substrate" "$KEY_VAULT_NAME")
# 2. Build environment-specific CORS origins
CORS_ORIGINS_ARRAY=()
if [ -n "$APP_GATEWAY_FQDN" ]; then
CORS_ORIGINS_ARRAY+=("https://${APP_GATEWAY_FQDN}")
CORS_ORIGINS_ARRAY+=("http://${APP_GATEWAY_FQDN}")
fi
# 3. Add dev/test/prod specific origins
if [ "$ENV" == "prod" ]; then
CORS_ORIGINS_ARRAY+=("https://${CUSTOM_DOMAIN}")
else
CORS_ORIGINS_ARRAY+=(
"http://localhost:3000"
"http://localhost:5173"
"http://localhost:8080"
)
fi
# 4. Deduplicate and pass to Bicep
CORS_ORIGINS=$(printf "%s," "${!unique_origins[@]}" | sed 's/,$//')
3. CORS Debugging Middleware
# app.py
@app.middleware("http")
async def cors_debug_middleware(request: Request, call_next):
origin = request.headers.get("origin")
# Log all preflight requests
if request.method == "OPTIONS" and origin:
logger.info(
"CORS Preflight",
extra={
"origin": origin,
"path": request.url.path,
"origin_allowed": origin in settings.cors_origins,
},
)
# Warn on rejected origins
elif settings.debug and origin:
if origin not in settings.cors_origins:
logger.warning(
"CORS Origin Not Allowed",
extra={
"origin": origin,
"allowed_origins_sample": settings.cors_origins[:5],
},
)
return await call_next(request)
4. Environment Variables Strategy
# Bicep sets these environment variables:
APP_CORS_ORIGINS="https://gateway.com,http://localhost:8080" # Explicit origins
APP_GATEWAY_URL="https://gateway.com" # For auto-detection in Python
# Docker Compose sets:
DOCKER_CONTAINER="true" # Enables Docker-specific origins
Consequences
Positive
- ✅ Zero CORS failures in Azure deployments with Application Gateway
- ✅ Automatic environment detection - no manual configuration needed
- ✅ Graceful degradation - falls back to localhost if nothing detected
- ✅ Explicit override - can still manually set
APP_CORS_ORIGINS - ✅ CORS debugging built-in - logs preflight requests and rejections
- ✅ Multi-environment support - dev/test/prod configurations automated
- ✅ Secure defaults - restrictive in prod, permissive in dev
Negative
- Complexity: More logic in config validation (mitigated by comprehensive docs)
- Testing: Need to test across all environments (addressed by test matrix)
- Debugging: Auto-detection may be "magical" (mitigated by logging)
Neutral
- Backward compatibility: Old explicit
APP_CORS_ORIGINSstill works - Performance: Negligible (~1ms at startup for validation)
Implementation
Files Changed
- apps/api/loan_defenders/api/config.py
- Made
cors_originsoptional with smart defaults - Added environment auto-detection logic
-
Added comprehensive logging
-
apps/api/loan_defenders/api/app.py
- Added
cors_debug_middlewarefor request logging - Logs preflight requests (OPTIONS)
-
Warns on CORS rejections in debug mode
-
infrastructure/scripts/deploy-apps.sh
- Retrieves Application Gateway FQDN from Key Vault
- Builds environment-specific CORS origins list
-
Passes both
APP_CORS_ORIGINSandAPP_GATEWAY_URL -
infrastructure/bicep/apps.bicep
- Added
APP_GATEWAY_URLenvironment variable -
Derives from
apiBaseUrlparameter -
docker-compose.yml
- Removed hardcoded
APP_CORS_ORIGINS -
Added
DOCKER_CONTAINER="true"marker -
infrastructure/bicep/environments/*/apps.json
- Updated default from port 3000 to 8080
Testing Strategy
# 1. Local development
export APP_CORS_ORIGINS="" # Test auto-detection
python -m uvicorn loan_defenders.api.app:app
# 2. Docker Compose
docker-compose up
# Check logs: "Auto-detected Docker deployment"
# 3. Azure with Application Gateway
./infrastructure/scripts/deploy-apps.sh dev ldfdev8-rg
az container logs --name <container-group> --container-name api | grep "CORS"
# Check logs: "Auto-detected Azure Application Gateway: https://..."
# 4. CORS preflight test
curl -X OPTIONS https://ldfdev8.eastus.cloudapp.azure.com/loan-defenders/api/chat \
-H "Origin: https://ldfdev8.eastus.cloudapp.azure.com" \
-v
Deployment
Rollout Plan
- Phase 1: Update API Config (Non-breaking)
- Deploy updated
config.pywith backward-compatible defaults -
Existing
APP_CORS_ORIGINSstill works -
Phase 2: Update Deployment Scripts
- Enhance
deploy-apps.shwith Application Gateway detection -
Scripts work with old and new API config
-
Phase 3: Update Docker Compose
- Remove hardcoded CORS, rely on auto-detection
-
Backward compatible (explicit override still possible)
-
Phase 4: Update Parameter Files
- Fix port 3000 → 8080 in
apps.json - Falls back to auto-detection if empty
Rollback
If issues arise:
# Override with explicit CORS at deployment time
export APP_CORS_ORIGINS="https://known-origin.com,http://localhost:8080"
./infrastructure/scripts/deploy-apps.sh dev <rg>
No code rollback needed - explicit APP_CORS_ORIGINS takes precedence.
Validation
Success Criteria
- Local
docker-compose upworks without CORS errors - Azure deployment with Application Gateway has no CORS errors
- Bastion Jump Box access via private IP works
- Production deployment with custom domain works
- CORS preflight requests logged correctly
- Rejected origins generate warnings in logs
Monitoring
# Check CORS configuration at startup
az container logs --name <container-group> --container-name api \
| grep "CORS.*configured"
# Monitor CORS rejections
az monitor log-analytics query \
--workspace <workspace-id> \
--analytics-query "ContainerLog | where Message contains 'CORS Origin Not Allowed'"
References
- MDN: Cross-Origin Resource Sharing (CORS)
- FastAPI CORS Middleware Documentation
- Azure Application Gateway Path-Based Routing
- ADR-049: ACI Private IP Networking
- ADR-054: Application Gateway in Substrate Layer
- CORS Configuration Guide
Decision Log
| Date | Decision | Rationale |
|---|---|---|
| 2025-10-29 | Initial implementation | Resolve persistent CORS failures in Azure deployments |
| 2025-10-29 | Add auto-detection | Reduce manual configuration burden |
| 2025-10-29 | Add debugging middleware | Enable troubleshooting of CORS issues |
| 2025-10-29 | Make explicit config optional | Support both manual and automatic modes |
Supersedes: Previous hardcoded CORS configuration
Superseded By: None
Amendment History: None