CORS (Cross-Origin Resource Sharing) web development’in en çok yanlış anlaşılan kavramlarından biri. Frontend developer “bir CORS error var” diyor, backend developer random header’lar ekliyor, sorun bazen çözülüyor bazen değil. Bu pattern’i kırmak için CORS’un nasıl çalıştığını gerçekten anlamak lazım.
15+ yıldır full-stack development yapıyorum, onlarca CORS issue debug ettim. Bu yazıda CORS mental model’ini ve common pitfalls’ı anlatacağım.
CORS ne için var?
Browser Same-Origin Policy (SOP) web’in güvenlik temeli. Bir origin (protocol + domain + port) başka origin’in kaynaklarını okuyamıyor. Bu kural olmasa:
- example.com’u ziyaret edince kötü niyetli example.com kodu bank.com’a senin cookie’lerinle request atardı
- Password’ün çalınabilirdi, hesap bilgilerin çekilebilirdi
SOP bunu engelliyor. Ama legitimate cross-origin ihtiyaçları da var: your-frontend.com’un your-api.com’a request atması.
CORS bu legitimate senaryoları enable eden mekanizma. Browser ile server arasında negotiate ediyor: “bu origin’den gelen request’i kabul ediyor musun?”
Simple request vs preflight
CORS iki kategoriye ayrılıyor:
Simple requests: Şu koşulları sağlıyorsa browser direct gönderiyor (preflight yok):
– Method: GET, HEAD, or POST
– Content-Type: application/x-www-form-urlencoded, multipart/form-data, text/plain
– Sadece standard header’lar (Accept, Content-Type, etc.)
Preflight requests: Yukarıdaki kriterler dışındaki her şey. Browser önce OPTIONS request atıyor, server “evet, bu request’i kabul ediyorum” derse gerçek request gidiyor.
Modern API’larda genelde JSON body + PUT/DELETE kullanılıyor, preflight default.
Preflight nasıl çalışıyor?
Browser API’ya POST request atacak, Content-Type: application/json ile. Simple değil, preflight gerekli.
Step 1: Browser OPTIONS gönderiyor:
OPTIONS /api/users HTTP/1.1
Host: api.example.com
Origin: https://app.example.com
Access-Control-Request-Method: POST
Access-Control-Request-Headers: Content-Type, AuthorizationStep 2: Server cevaplıyor:
HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Methods: POST, GET, DELETE
Access-Control-Allow-Headers: Content-Type, Authorization
Access-Control-Max-Age: 86400Step 3: Browser gerçek POST gönderiyor:
POST /api/users HTTP/1.1
Host: api.example.com
Origin: https://app.example.com
Content-Type: application/json
Authorization: Bearer token...
{...}Preflight’i unutma: her complex request’te bu cycle. Extra latency var ama 1 kez. Max-Age ile browser response’u cache ediyor (default 5 dakika, browser-specific).
Yaygın CORS hataları
Hata 1: “No ‘Access-Control-Allow-Origin’ header is present”
En sık hata. Backend CORS header göndermiyor veya browser’ın beklediği origin’i göndermiyor.
Fix:
Access-Control-Allow-Origin: https://app.example.comveya wildcard (dikkat! aşağıya bakın):
Access-Control-Allow-Origin: *Hata 2: Wildcard with credentials
Access-Control-Allow-Origin: *
Access-Control-Allow-Credentials: trueBrowser bunu reject ediyor. Wildcard ile credentials (cookie, Authorization header) kombine edilemiyor.
Fix: Specific origin kullanmalı:
Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Credentials: trueMultiple origin destekliyorsan, request’te gelen Origin’i dynamically echo et (validation ile).
Hata 3: OPTIONS request authentication’a takılıyor
Authentication middleware OPTIONS request’i de block ediyor. Preflight check yapılmadan 401 dönüyor.
Fix: OPTIONS request’i authentication middleware’den muaf tut.
# Example (Python/Flask-style)
@app.before_request
def authenticate():
if request.method == 'OPTIONS':
return # Skip auth for preflight
# ... normal auth ...Hata 4: Header’ı allow etmiyorsun
Frontend custom header gönderiyor (örn. X-Requested-With). Backend Access-Control-Allow-Headers‘ta bu header’ı listede tutmuyor.
Fix:
Access-Control-Allow-Headers: Content-Type, Authorization, X-Requested-WithTüm custom header’ları explicitly listele.
Hata 5: Response header’ı expose etmemek
Default olarak browser sadece basic header’ları JavaScript’e expose ediyor. Custom header (örn. X-Total-Count) için:
Access-Control-Expose-Headers: X-Total-Count, X-Pagination-InfoBu olmadan frontend custom header’ları okuyamıyor.
Credentials mode
Fetch API’da credentials: 'include' ile cookie/auth header gönderiliyor.
Frontend:
fetch('https://api.example.com/users', {
credentials: 'include',
headers: {'Content-Type': 'application/json'}
})Backend:
Access-Control-Allow-Origin: https://app.example.com // NOT *
Access-Control-Allow-Credentials: trueCredentials mode’da wildcard yok. Specific origin zorunlu.
Wildcard vs specific origin
Wildcard (*):
– Simple: tüm origin’lere izin
– Credentials kullanılamaz
– Public API’lar için OK (örn. content API, no auth)
Specific origin (domain):
– Sadece belirli domain’den izin
– Credentials kullanılabilir
– Secure, recommended for authenticated APIs
Multiple origin destekliyorsan whitelist kur:
allowed_origins = ['https://app.example.com', 'https://staging.example.com']
@app.after_request
def add_cors_headers(response):
origin = request.headers.get('Origin')
if origin in allowed_origins:
response.headers['Access-Control-Allow-Origin'] = origin
response.headers['Access-Control-Allow-Credentials'] = 'true'
return responseSubdomain CORS
app.example.com → api.example.com request’i de cross-origin. Subdomain aynı bile olsa browser bunu cross olarak görüyor.
Fix aynı: specific origin whitelist.
Cookie tarafında: cookie’yi .example.com domain’ine yazıyorsan (leading dot), subdomain’ler erişiyor. example.com domain’ine yazarsan sadece aynı origin.
CORS vs CSRF
Sıkça karışıyor. Farkları:
CORS: Browser’ın cross-origin request’lere izin verip vermediğini kontrol ediyor. Server’ın JavaScript’e hangi response’u göstereceğini belirliyor.
CSRF: User’ın bilgisi olmadan cross-site request gönderilmesini önleyen pattern. CSRF token ile mitigate.
CORS güvenlik değil, access control. CSRF saldırısını CORS engelliyor ama CORS yeterli değil (legacy browser, specific scenarios). CSRF token hâlâ gerekli.
Development’ta CORS
Local development’ta API farklı port’ta:
– Frontend: localhost:3000
– Backend: localhost:8080
İkisi farklı origin. CORS hatası.
Fix options:
1. Backend’de development CORS:
if environment == 'development':
allowed_origins.append('http://localhost:3000')2. Proxy: Webpack dev server / Vite proxy ile frontend aynı origin’de gibi davranıyor.
// vite.config.js
export default {
server: {
proxy: {
'/api': 'http://localhost:8080'
}
}
}Frontend’den /api/users → Vite proxy → localhost:8080/api/users. Same-origin’den gidiyor gibi görünüyor.
Production’da proxy yok, CORS config production’da kurulacak.
Debugging CORS
Network tab’da preflight’ı görebilirsin:
- OPTIONS request var mı? Yoksa browser preflight sending etmiyor.
- OPTIONS response code ne? 200/204 olmalı. 401/403 ise auth issue.
- Access-Control-Allow-Origin header ne? Request Origin ile match mı?
- Access-Control-Allow-Methods method’u listeliyor mu?
- Access-Control-Allow-Headers custom header’ları listeliyor mu?
Bu 5 soruyu cevaplayınca CORS issue’larının %95’i çözülüyor.
Sonuç
CORS browser’ın güvenlik mekanizması, server restrictions değil. Backend doğru header’ları göndermezse browser request’i block ediyor.
Key concepts: simple vs preflight, credentials restriction, specific origin vs wildcard. Bunları anladığında debug 5 dakikada bitiyor.
Production’da CORS config dikkatli kur: specific origins, minimal allowed methods/headers, credentials needed only when required. Security ve UX dengesi.