🛡️ CORS Issues - Troubleshooting Guide
Getting CORS errors? This guide will help you diagnose and fix Cross-Origin Resource Sharing issues.
Quick Fix: In 99% of cases, CORS is automatically configured correctly. If you're seeing errors, use this guide to understand what's happening and how to fix it.
📋 Table of Contents
- Common Error Messages
- Understanding CORS
- Diagnostic Steps
- Common Issues & Solutions
- How Auto-Configuration Works
- Manual Configuration
- Security Best Practices
🚨 Common Error Messages
Error 1: Origin Not Allowed
Browser Console:
Access to fetch at 'https://api.example.com/loan-defenders/api/chat' from origin
'https://ui.example.com' has been blocked by CORS policy:
No 'Access-Control-Allow-Origin' header is present on the requested resource.
What it means: - The API doesn't include your UI's origin in its allowed CORS origins list - The browser is blocking the request for security reasons
Quick fix: → See Origin Not in Allowed List
Error 2: Preflight Request Failed
Browser Console:
Access to fetch at 'https://api.example.com/loan-defenders/api/chat' from origin
'https://ui.example.com' has been blocked by CORS policy:
Response to preflight request doesn't pass access control check:
No 'Access-Control-Allow-Origin' header is present.
What it means: - Browser sent OPTIONS preflight request first - API didn't respond correctly to OPTIONS request
Quick fix: → See Preflight Request Failures
Error 3: Wildcard with Credentials
Browser Console:
Access to fetch at 'https://api.example.com/loan-defenders/api/chat' from origin
'https://ui.example.com' has been blocked by CORS policy:
The value of the 'Access-Control-Allow-Origin' header must not be
the wildcard '*' when credentials are included.
What it means:
- API is using wildcard "*" for allowed origins
- Cannot use wildcard when sending cookies/credentials
Quick fix: → See Wrong Configuration
🔍 Understanding CORS
What is CORS?
CORS (Cross-Origin Resource Sharing) is a browser security feature that: - Blocks web pages from making requests to different domains - Requires the API server to explicitly allow specific origins - Protects users from malicious cross-site requests
The CORS Request Flow
┌─────────────────────────────────────────────────────────────────┐
│ 1. Browser loads UI from: https://gateway.com/ │
└─────────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────────┐
│ 2. UI JavaScript makes API call: │
│ fetch('https://gateway.com/loan-defenders/api/chat') │
└─────────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────────┐
│ 3. Browser adds Origin header: │
│ Origin: https://gateway.com │
└─────────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────────┐
│ 4. API checks allowed origins: │
│ ├─ Is "https://gateway.com" in settings.cors_origins? │
│ ├─ ✅ YES → Add Access-Control-Allow-Origin header │
│ └─ ❌ NO → Reject (CORS error) │
└─────────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────────┐
│ 5. Browser receives response: │
│ ├─ Has Access-Control-Allow-Origin header matching origin? │
│ ├─ ✅ YES → Request succeeds, data delivered to JavaScript │
│ └─ ❌ NO → CORS error, JavaScript cannot access response │
└─────────────────────────────────────────────────────────────────┘
What is a Preflight Request?
For "complex" requests (POST with JSON, custom headers, etc.), the browser:
1. Sends OPTIONS request first (preflight):
OPTIONS /loan-defenders/api/chat HTTP/1.1
Host: api.example.com
Origin: https://ui.example.com
Access-Control-Request-Method: POST
Access-Control-Request-Headers: Content-Type, Authorization
2. API must respond with permission:
HTTP/1.1 200 OK
Access-Control-Allow-Origin: https://ui.example.com
Access-Control-Allow-Methods: POST, GET, OPTIONS
Access-Control-Allow-Headers: Content-Type, Authorization
Access-Control-Allow-Credentials: true
3. If preflight succeeds, browser sends actual request
4. If preflight fails, browser blocks the actual request
🔧 Diagnostic Steps
Step 1: Check What Origin the Browser is Sending
Open Browser DevTools:
1. Press F12 to open DevTools
2. Go to Network tab
3. Reproduce the CORS error
4. Click on the failed request
5. Go to Headers tab
6. Look at Request Headers → Find Origin:
Example:
This is the EXACT origin that must be in the API's allowed list.
Step 2: Check API CORS Configuration
Docker (local development):
Azure:
az container logs \
--name <container-group-name> \
--resource-group <resource-group> \
--container-name api \
| grep "CORS"
Expected output:
[CORS] Auto-detected Azure Application Gateway: https://ldfdev8.eastus.cloudapp.azure.com
[CORS] Configured origins: https://ldfdev8.eastus.cloudapp.azure.com, http://localhost:8080... (12 total)
If you see:
- Auto-detected Azure Application Gateway → Good! Gateway URL is configured
- Auto-detected Docker deployment → Good! Docker origins configured
- Configured origins: ... → Shows all allowed origins
If you DON'T see CORS logs:
- API might not be starting correctly
- Check full logs: docker-compose logs api or az container logs ...
Step 3: Verify Environment Variables
Docker:
Expected:
Azure:
az container show \
--name <container-group-name> \
--resource-group <resource-group> \
--query "containers[?name=='api'].environmentVariables" \
-o table
Look for:
- APP_CORS_ORIGINS - List of allowed origins
- APP_GATEWAY_URL - Application Gateway URL (Azure only)
- DOCKER_CONTAINER - Marker for Docker environment
Step 4: Test CORS Manually
Test preflight request:
curl -X OPTIONS http://localhost:8000/loan-defenders/api/chat \
-H "Origin: http://localhost:8080" \
-H "Access-Control-Request-Method: POST" \
-H "Access-Control-Request-Headers: Content-Type" \
-i
# Expected response:
# HTTP/1.1 200 OK
# Access-Control-Allow-Origin: http://localhost:8080
# Access-Control-Allow-Methods: POST, GET, OPTIONS, ...
# Access-Control-Allow-Headers: Content-Type
Test actual POST request:
curl -X POST http://localhost:8000/loan-defenders/api/chat \
-H "Origin: http://localhost:8080" \
-H "Content-Type: application/json" \
-d '{"user_message": "test", "session_id": "test123"}' \
-i
# Expected response:
# HTTP/1.1 200 OK
# Access-Control-Allow-Origin: http://localhost:8080
🐛 Common Issues & Solutions
Issue 1: Origin Not in Allowed List
Symptoms:
- CORS error in browser console
- API logs show: [WARN] CORS Origin Not Allowed: https://unexpected-origin.com
Diagnosis:
# Check what origins are allowed
docker-compose logs api | grep "Configured origins"
# Check what origin browser is sending
# DevTools → Network → Failed request → Headers → Origin
Solution A: Redeploy (Azure)
If Application Gateway wasn't detected during deployment:
# Redeploy Apps layer (automatically detects Application Gateway)
./infrastructure/scripts/deploy-apps.sh dev <resource-group>
Solution B: Add Custom Origin
For production custom domains:
# Set custom domain before deployment
export CUSTOM_DOMAIN="yourdomain.com"
./infrastructure/scripts/deploy-apps.sh prod <resource-group>
Solution C: Temporary Override
For testing or one-off fixes:
# Docker: Edit docker-compose.yml
api:
environment:
APP_CORS_ORIGINS: "https://custom-origin.com,http://localhost:8080"
# Then restart
docker-compose down && docker-compose up
# Azure: Set env var and redeploy
export APP_CORS_ORIGINS="https://custom-origin.com,http://localhost:8080"
./infrastructure/scripts/deploy-apps.sh dev <resource-group>
Issue 2: Preflight Request Failures
Symptoms: - Browser shows preflight error - API returns 404 or 405 for OPTIONS requests - Works with simple GET, fails with POST
Diagnosis:
# Test OPTIONS request manually
curl -X OPTIONS http://localhost:8000/loan-defenders/api/chat \
-H "Origin: http://localhost:8080" \
-i
# Should return 200 OK, not 404/405
Solution:
This should NOT happen with our API (FastAPI handles OPTIONS automatically via CORSMiddleware).
If it does happen:
1. Check that CORSMiddleware is registered in app.py
2. Check nginx.conf doesn't block OPTIONS requests
3. Check Application Gateway routing
Check middleware registration:
# apps/api/loan_defenders/api/app.py
app.add_middleware(CORSMiddleware, **settings.get_cors_config())
# ↑ This line MUST be present
Issue 3: Using Wildcard with Credentials
Symptoms: - Error mentions "wildcard '*' when credentials are included"
Diagnosis:
Solution:
Never use wildcards in production!
# ❌ BAD - Security risk
cors_origins = ["*"]
# ✅ GOOD - Explicit origins
cors_origins = [
"https://yourdomain.com",
"https://app.yourdomain.com",
]
Our auto-configuration never uses wildcards, but if you manually override, ensure you list specific origins.
Issue 4: Application Gateway FQDN Not Detected
Symptoms: - Azure deployment - CORS logs don't show Application Gateway - Browser origin is gateway URL but it's not allowed
Diagnosis:
# Check if Application Gateway is deployed
az network application-gateway list \
--resource-group <resource-group> \
-o table
# Check Key Vault for gateway FQDN
az keyvault secret show \
--vault-name <key-vault-name> \
--name "substrate-appGatewayPublicFqdn" \
--query value \
-o tsv
Solution:
# 1. Deploy Substrate layer (if not already)
./infrastructure/scripts/deploy-substrate.sh dev <resource-group> --deploy-app-gateway
# 2. Redeploy Apps layer to detect gateway
./infrastructure/scripts/deploy-apps.sh dev <resource-group>
Issue 5: Wrong Port in Origin
Symptoms:
- Local development
- UI at localhost:8080 but API allows localhost:3000
Diagnosis:
# Check allowed origins
docker-compose logs api | grep "Configured origins"
# Check UI port
docker-compose ps ui
# Look at PORTS column
Solution:
This is already fixed! Our config now uses correct port 8080.
If you still see issues, check docker-compose.yml:
⚙️ How Auto-Configuration Works
Environment Detection Priority
1. Explicit Configuration (APP_CORS_ORIGINS env var)
├─ If set → Use exactly these origins
└─ If not set → Continue to auto-detection
2. Azure Application Gateway (APP_GATEWAY_URL env var)
├─ If set → Add gateway URL + http/https variants
└─ If not set → Continue to next check
3. Docker Environment (DOCKER_CONTAINER env var or /.dockerenv file)
├─ If detected → Add docker-compose service names + localhost
└─ If not detected → Continue to defaults
4. Default Localhost Ports (Always added)
└─ Add common dev ports: 3000, 5173, 8080
Where Auto-Detection Happens
API Config (apps/api/loan_defenders/api/config.py):
@field_validator("cors_origins", mode="before")
@classmethod
def parse_cors_origins(cls, v: Any) -> list[str]:
if v is not None:
return [origin.strip() for origin in v.split(",")]
# Auto-detect environment
origins = []
# 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://"))
# Check for Docker
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
What Gets Logged
Startup logs show:
[CORS] Auto-detected Azure Application Gateway: https://ldfdev8.eastus.cloudapp.azure.com
[CORS] Auto-detected Docker deployment
[CORS] Configured origins: https://ldfdev8.eastus.cloudapp.azure.com, http://ui:8080... (12 total)
During requests (debug mode):
[INFO] CORS Preflight Request: origin=https://gateway.com, allowed=True
[WARN] CORS Origin Not Allowed: https://unexpected-origin.com
🔧 Manual Configuration
Override Auto-Configuration
Local Development (.env file):
Docker Compose (docker-compose.yml):
Azure Deployment:
export APP_CORS_ORIGINS="https://yourdomain.com,https://www.yourdomain.com"
./infrastructure/scripts/deploy-apps.sh prod <resource-group>
Add Origin Permanently
Edit deployment script (infrastructure/scripts/deploy-apps.sh):
# Around line 670
CORS_ORIGINS_ARRAY+=(
"http://localhost:3000"
"http://localhost:5173"
"http://localhost:8080"
"https://your-custom-domain.com" # ← Add here
)
Then redeploy:
🔐 Security Best Practices
✅ DO
1. Use explicit origins in production:
2. Include only trusted domains:
cors_origins = [
"https://yourdomain.com", # Your domain
"https://app.yourdomain.com", # Your subdomain
]
3. Use HTTPS in production:
4. Keep localhost for development:
❌ DON'T
1. Never use wildcards in production:
2. Don't include untrusted domains:
3. Don't mix http/https in production:
4. Don't disable CORS in production:
🆘 Still Having Issues?
Enable Debug Logging
Docker:
Azure:
# Check logs with more detail
az container logs \
--name <container-group> \
--resource-group <rg> \
--container-name api \
--tail 100
Check Browser DevTools
- Open DevTools (
F12) - Console tab - Look for CORS errors
- Network tab - Find failed request
- Click request → Headers tab
- Check Request Headers →
Origin: - Check Response Headers →
Access-Control-Allow-Origin: - Compare Origin (sent) vs Access-Control-Allow-Origin (received)
Contact Support
If you've tried everything and CORS still fails:
-
Gather diagnostic info:
# Get CORS configuration docker-compose logs api | grep "CORS" > cors-logs.txt # Get environment variables docker-compose exec api printenv | grep -E "CORS|GATEWAY" > env-vars.txt # Get browser DevTools screenshot # - Network tab showing failed request # - Headers showing Origin and Access-Control headers -
Create GitHub issue with:
- Error message from browser console
- CORS logs from API
- Environment variables (redact sensitive data)
- Browser DevTools screenshot
-
Deployment environment (local/Azure/prod)
-
Slack channel:
#loan-defenders-support
📚 Additional Resources
- CORS Configuration Guide - Complete technical documentation
- ADR-056: Intelligent CORS - Architecture decision record
- MDN: CORS - Official CORS documentation
- FastAPI CORS Middleware - FastAPI CORS docs
Last Updated: 2025-10-29
Maintained By: Infrastructure Team