Skip to content

🛡️ 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

  1. Common Error Messages
  2. Understanding CORS
  3. Diagnostic Steps
  4. Common Issues & Solutions
  5. How Auto-Configuration Works
  6. Manual Configuration
  7. 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:

Origin: https://ldfdev8.eastus.cloudapp.azure.com

This is the EXACT origin that must be in the API's allowed list.


Step 2: Check API CORS Configuration

Docker (local development):

docker-compose logs api | grep "CORS"

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:

docker-compose exec api printenv | grep -E "CORS|GATEWAY|DOCKER"

Expected:

DOCKER_CONTAINER=true
APP_CORS_ORIGINS=http://localhost:8080,...

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:

# Check CORS configuration
docker-compose logs api | grep "CORS"

# Look for: cors_origins = ["*"]

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:

ui:
  ports:
    - "8080:8080"  # Must match allowed origin port


⚙️ 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):

APP_CORS_ORIGINS="http://localhost:5173,http://localhost:3000,http://localhost:8080"

Docker Compose (docker-compose.yml):

services:
  api:
    environment:
      APP_CORS_ORIGINS: "http://localhost:5173,http://localhost:8080"

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:

./infrastructure/scripts/deploy-apps.sh dev <resource-group>


🔐 Security Best Practices

✅ DO

1. Use explicit origins in production:

APP_CORS_ORIGINS="https://yourdomain.com,https://www.yourdomain.com"

2. Include only trusted domains:

cors_origins = [
    "https://yourdomain.com",      # Your domain
    "https://app.yourdomain.com",  # Your subdomain
]

3. Use HTTPS in production:

cors_origins = ["https://yourdomain.com"]  # Not http://

4. Keep localhost for development:

if ENV == "dev":
    cors_origins.extend(["http://localhost:8080"])

❌ DON'T

1. Never use wildcards in production:

cors_origins = ["*"]  # ← SECURITY RISK! Allows ANY origin

2. Don't include untrusted domains:

cors_origins = ["https://random-site.com"]  # ← Why is this here?

3. Don't mix http/https in production:

cors_origins = [
    "http://yourdomain.com",   # ← Should be https
    "https://yourdomain.com",
]

4. Don't disable CORS in production:

# ❌ NEVER DO THIS
app.add_middleware(CORSMiddleware, allow_origins=["*"])


🆘 Still Having Issues?

Enable Debug Logging

Docker:

# docker-compose.yml
api:
  environment:
    APP_DEBUG: "true"
    APP_LOG_LEVEL: "DEBUG"

Azure:

# Check logs with more detail
az container logs \
    --name <container-group> \
    --resource-group <rg> \
    --container-name api \
    --tail 100

Check Browser DevTools

  1. Open DevTools (F12)
  2. Console tab - Look for CORS errors
  3. Network tab - Find failed request
  4. Click request → Headers tab
  5. Check Request HeadersOrigin:
  6. Check Response HeadersAccess-Control-Allow-Origin:
  7. Compare Origin (sent) vs Access-Control-Allow-Origin (received)

Contact Support

If you've tried everything and CORS still fails:

  1. 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
    

  2. Create GitHub issue with:

  3. Error message from browser console
  4. CORS logs from API
  5. Environment variables (redact sensitive data)
  6. Browser DevTools screenshot
  7. Deployment environment (local/Azure/prod)

  8. Slack channel: #loan-defenders-support


📚 Additional Resources


Last Updated: 2025-10-29
Maintained By: Infrastructure Team