Skip to content
ENTERPRISE Enterprise technology · Engineered for your business Enterprise Software · Managed Servers · Business E-mail · LiteSpeed + CloudLinux · Imunify360 Security · cPanel Control · Network profile up to 3 Gbps on eligible plans
AIOR

CORS Error Fix: Access-Control-Allow-Origin, Preflight and Credentials

Aior

Administrator

Staff member
Joined
Apr 2, 2023
Messages
929
Reaction score
2
Points
18
Age
41
Location
Turkey
Website
aior.com
Thread owner

Tarayıcı konsolunda "has been blocked by CORS policy" yazıyor. Ön yüzünüz (front-end) http://localhost:5173 üzerinden http://localhost:8000 adresindeki API'ye bağlanıyor ya da canlı ortamda https://app.example.com sayfası https://api.example.com adresine istek atıyor; üstelik aynı istek curl ve Postman ile sorunsuz çalışıyor. Sorunun özü bu karşıtlıkta: CORS (Cross-Origin Resource Sharing, kökenler arası kaynak paylaşımı) kuralını tarayıcı uygular, izni ise isteği yanıtlayan sunucu verir. Bu rehberde hata mesajını nasıl okuyacağınızı, eksik başlığı (header) DevTools ve curl ile nasıl bulacağınızı ve CORS hatasının Express, Django, FastAPI, Laravel, Spring Boot, ASP.NET Core, Nginx ve Apache tarafında güvenli ve kalıcı olarak nasıl çözüleceğini adım adım anlatıyoruz.

Kısa cevap: API sunucusu Access-Control-Allow-Origin başlığında sayfanızın kökenini birebir döndürmeli, OPTIONS ön kontrol isteğine (preflight) kullandığınız yöntem ve başlıkları listeleyen 2xx bir yanıt vermeli; çerez gönderiyorsanız * yerine açık bir kökenle birlikte Access-Control-Allow-Credentials: true da eklemelidir.

Chrome ve Firefox'ta CORS hata mesajları​

Chrome ve Edge gibi Chromium tabanlı tarayıcılar mesajı aşağıdaki biçimde yazar. "CORS policy:" ifadesinden sonra gelen kısım neyin başarısız olduğunu söyler:

Code:
Access to fetch at 'https://api.example.com/v1/orders' from origin 'https://app.example.com' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.

Access to XMLHttpRequest at 'https://api.example.com/v1/orders' from origin 'https://app.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 on the requested resource.

Access to fetch at 'https://api.example.com/v1/me' from origin 'https://app.example.com' has been blocked by CORS policy: The value of the 'Access-Control-Allow-Origin' header in the response must not be the wildcard '*' when the request's credentials mode is 'include'.

En sık karşılaşılan, ilk satırdaki access-control-allow-origin hatasıdır: yanıtta bu başlık hiç yoktur. Firefox aynı sorunları kendi cümleleriyle bildirir:

Code:
Cross-Origin Request Blocked: The Same Origin Policy disallows reading the remote resource at https://api.example.com/v1/orders. (Reason: CORS header ‘Access-Control-Allow-Origin’ missing). Status code: 200.

Cross-Origin Request Blocked: The Same Origin Policy disallows reading the remote resource at https://api.example.com/v1/orders. (Reason: CORS preflight response did not succeed). Status code: 401.

Cross-Origin Request Blocked: The Same Origin Policy disallows reading the remote resource at ‘https://api.example.com/v1/me’. (Reason: Credential is not supported if the CORS header ‘Access-Control-Allow-Origin’ is ‘*’).

JavaScript tarafında ise yalnızca genel bir hata yakalarsınız: Chrome'da TypeError: Failed to fetch, Firefox'ta TypeError: NetworkError when attempting to fetch resource., axios'ta AxiosError: Network Error. Tarayıcı asıl nedeni betiklerden bilerek gizler ve yalnızca konsola yazar. Chrome 135 ve öncesi, fetch ile yapılan isteklerde eksik başlık mesajının sonuna "If an opaque response serves your needs, set the request's mode to 'no-cors'..." önerisini de eklerdi; bu öneri hiçbir şeyi çözmez (aşağıdaki geliştirme ortamı bölümüne bakın).

Arka planda ne oluyor?​

  • Tarayıcı isteği (gerekiyorsa önce bir ön kontrol isteğini) gönderir, yanıtı alır ve CORS başlıklarını denetler. Başlıklar sayfanızın kökenine izin vermiyorsa yanıt kodunuza hiç ulaşmaz.
  • Ön kontrol başarısız olursa asıl istek hiç gönderilmez. Basit bir istek denetime takıldığında ise sunucu isteği çoktan almış, hatta işlemiş olabilir. CORS, isteği kimin gönderebileceğini değil, yanıtı kimin okuyabileceğini belirler.
  • curl, Postman, mobil uygulamalar ve sunucudan sunucuya çağrılar CORS denetimi yapmaz. "Postman'de çalışıyor" demek bu yüzden CORS ayarınız hakkında hiçbir şey kanıtlamaz.
  • Çözüm sunucuda ya da önündeki ters vekil sunucuda (reverse proxy) yapılır: sunucu Access-Control-* başlıklarını göndererek izin verir. Ön yüzdeki hiçbir ayar bu izni kendi başına veremez.

Köken (origin) nedir: şema, alan adı ve port​

Köken, şema://alan-adı:port üçlüsüdür. İki adres ancak üç parça da aynıysa aynı kökendendir; yolun (path) bir önemi yoktur.

Sayfaİsteğin gittiği adresAynı köken mi?Neden
https://app.example.comhttps://app.example.com/api/ordersEvetYalnızca yol farklı
http://localhost:3000http://localhost:8000HayırPort farklı
http://localhost:5173http://127.0.0.1:5173HayırAna makine adı (host) farklı
http://example.comhttps://example.comHayırŞema farklı
https://www.example.comhttps://example.comHayırwww ile kök alan adı (apex) ayrı sunucu adlarıdır
https://app.example.comhttps://api.example.comHayırAlt alan adı farklı, ama site aynı (çerez bölümüne bakın)

Tarayıcı sayfanın kökenini Origin istek başlığına yol bilgisi ve sondaki eğik çizgi olmadan yazar. Sunucu Access-Control-Allow-Origin başlığında tam olarak bu metni (ya da *) döndürmek zorundadır; ayarlarınıza sonunda eğik çizgiyle yazılmış https://app.example.com/ değeri hiçbir zaman eşleşmez.

Basit istek ve ön kontrollü (preflight) istek farkı​

Bir istek ancak aşağıdakilerin hepsi doğruysa "basit" sayılır ve doğrudan gönderilir:
  • Yöntem GET, HEAD veya POST'tur.
  • Kodunuz yalnızca CORS'un serbest bıraktığı (CORS-safelisted) başlıkları ayarlar: Accept, Accept-Language, Content-Language, Content-Type ve tek aralıklı bir Range.
  • Content-Type varsa değeri application/x-www-form-urlencoded, multipart/form-data veya text/plain'dir.
  • Gövde bir ReadableStream değildir ve XMLHttpRequest yükleme (upload) olaylarına dinleyici eklenmemiştir.
Bunun dışındaki her istekte tarayıcı önce bir ön kontrol isteği gönderir: Origin, Access-Control-Request-Method ve Access-Control-Request-Headers başlıklarını taşıyan bir OPTIONS isteği. Gerçek projelerde en sık tetikleyiciler PUT, PATCH ve DELETE yöntemleri, Authorization: Bearer başlığı, X-Requested-With veya X-Tenant-Id gibi özel başlıklar ve Content-Type: application/json'dır. Kısacası bir API'ye JSON gönderen POST isteklerinin neredeyse tamamı ön kontrollüdür. Ön kontrol isteği hiçbir zaman çerez ya da Authorization taşımaz; yanıtı da yönlendirme içermeyen bir 2xx (200 veya 204) olmalıdır.

İstek başlığı (tarayıcı)Yanıt başlığı (sunucu)Kural
OriginAccess-Control-Allow-OriginTek değer: isteyen köken ya da *; kimlik bilgili isteklerde * reddedilir
Access-Control-Request-Method (ön kontrol)Access-Control-Allow-Methodsİstenen yöntemi içermeli; GET, HEAD ve POST her zaman serbesttir
Access-Control-Request-Headers (ön kontrol)Access-Control-Allow-Headersİstenen her başlığı listelemeli; * yalnızca kimlik bilgisi taşımayan isteklerde joker sayılır ve Fetch standardı Authorization'ı bu jokerin dışında tutar
Çerez, credentials: 'include'Access-Control-Allow-CredentialsGeçerli tek değer true'dur ve açık bir köken ister
(betik bir yanıt başlığını okur)Access-Control-Expose-HeadersCache-Control, Content-Language, Content-Length, Content-Type, Expires, Last-Modified ve Pragma dışındaki her başlık için gerekir
(ön kontrol önbelleği)Access-Control-Max-AgeSaniye cinsinden; Chromium 7200, Firefox 86400 saniyeyle sınırlar; varsayılan 5'tir
(proxy, CDN, tarayıcı önbelleği)Vary: OriginAllow-Origin isteğe göre değişiyorsa gönderilmelidir

Tarayıcılar Access-Control-Request-Headers içindeki başlık adlarını küçük harfe çevirip sıralar (örneğin authorization,content-type). Başlık adlarında büyük-küçük harf ayrımı olmadığından sunucu bunları istediği yazımla listeleyebilir. Joker (*) hakkında bir not: Chrome ve Firefox, kimlik bilgisi taşımayan isteklerde joker değerin Authorization'ı da kapsamasına şimdilik izin veriyor; ancak standart buna izin vermediği için ikisi de kullanımdan kaldırma (deprecation) uyarısı gösteriyor. Bu yüzden Authorization'ı adıyla yazın.

CORS hatasını beş adımda teşhis edin​

  1. DevTools'ta doğru satırı bulun. Ağ (Network) panelini açın, All filtresini seçin ve hatayı yeniden tetikleyin. Ön kontrol satırları Other kategorisinde yer aldığı için Fetch/XHR filtresi onları gizler. Chrome'da başarısız çağrının Status sütununda CORS error yazar; imleci üzerine getirince hata kodu görünür. Bir sütun başlığına sağ tıklayıp Method sütununu ekleyin: ön kontrollü bir çağrı POST + Preflight olarak görünür ve Type değeri preflight olan OPTIONS satırına bağlantı verir. Firefox ise OPTIONS isteğini ayrı bir satır olarak listeler.
  2. Ön kontrol yanıtının durum koduna bakın. 2xx olmalıdır. 301, 302, 307 veya 308 bir yönlendirme demektir (Chrome: "Redirect is not allowed for a preflight request"). 401 veya 403, kimlik doğrulamanın ya da bir WAF'ın CORS'tan önce devreye girdiğini; 404 veya 405, o yolda OPTIONS'a yanıt veren bir şey olmadığını; 5xx ise uygulamanın hata verdiğini gösterir.
  3. Yanıt başlıklarını okuyun. Access-Control-Allow-Origin sayfa kökeniyle karakteri karakterine aynı olmalı ve yalnızca bir kez bulunmalı; Allow-Methods ve Allow-Headers tarayıcının istediklerini kapsamalı; çerez söz konusuysa Allow-Credentials true olmalıdır.
  4. curl ile yeniden üretin. Böylece sunucudaki her değişikliği tarayıcıya gerek kalmadan test edersiniz (komutlar aşağıda).
  5. Hata yollarını da kontrol edin. 401, 404 ve 500 yanıtları da CORS başlıklarını taşımalıdır; taşımıyorsa tarayıcı gerçek hatanın yerine CORS hatası gösterir.

Bash:
# 1) Ön kontrol isteği, tarayıcının gönderdiği biçimde
curl -i -X OPTIONS https://api.example.com/v1/orders \
  -H "Origin: https://app.example.com" \
  -H "Access-Control-Request-Method: POST" \
  -H "Access-Control-Request-Headers: authorization,content-type"

# 2) Asıl istek ve bir hata yanıtı da başlıkları taşımalı
curl -i https://api.example.com/v1/orders -H "Origin: https://app.example.com"
curl -i https://api.example.com/v1/olmayan-yol -H "Origin: https://app.example.com"

Sağlıklı bir ön kontrol yanıtı kabaca şöyle görünür (başlıkların sırası ve harf yazımı sunucuya göre değişir):

Code:
HTTP/2 204
access-control-allow-origin: https://app.example.com
access-control-allow-methods: GET, POST, PUT, PATCH, DELETE
access-control-allow-headers: Authorization, Content-Type
access-control-allow-credentials: true
access-control-max-age: 600
vary: Origin

404 ya da 401 yanıtında access-control-allow-origin yoksa o yolu da düzeltin: Nginx'te always, Apache'de Header always kullanın ve uygulamada CORS ara katmanını (middleware) hata işleyicilerinin önüne alın. Tutarlı hata gövdeleri için REST API tasarımı rehberimize bakabilirsiniz.

Hızlı başvuru: mesaj, neden, çözüm​

Mesajda geçen ifadeOlası nedenÇözüm
No 'Access-Control-Allow-Origin' header is presentBu yol ya da köken için CORS ayarı yok veya yanıt, proxy'nin ya da uygulamanın hata sayfasıKökeni izin listesine ekleyin; hata yanıtlarına da başlık ekleyin
Response to preflight request doesn't pass access control checkOPTIONS isteği 401/403/404/405 aldı ya da başlıksız döndüOPTIONS'ı kimlik doğrulamadan önce, 204 ve Allow-* başlıklarıyla yanıtlayın
Redirect is not allowed for a preflight requesthttp'den https'e, sondaki eğik çizgi ya da giriş sayfası yönlendirmesiSon adresi doğrudan çağırın
must not be the wildcard '*' when the request's credentials mode is 'include'Kimlik bilgili isteğe Allow-Origin: * ile yanıt verilmişİzinli kökeni aynen döndürün, Allow-Credentials: true gönderin
contains multiple values ..., but only one is allowedBaşlığı hem proxy hem uygulama ekliyor ya da tek başlıkta birden çok köken varCORS'u tek katmanda yapın; her yanıtta tek köken gönderin
Method PUT is not allowed by Access-Control-Allow-MethodsYöntem ön kontrol yanıtında yokİzinli yöntemlere ekleyin
Request header field authorization is not allowed by Access-Control-Allow-HeadersBaşlık Allow-Headers listesinde yokAdıyla yazın; kimlik bilgili isteklerde * joker sayılmaz
has a value '...' that is not equal to the supplied originSabit yazılmış köken farklı (www, port, http/https, sondaki eğik çizgi)Köken metnini birebir eşleştirin

Express, Django, Laravel, Nginx ve diğerleri için CORS ayarı​

Örneklerin hepsinde https://app.example.com yerine kendi ön yüz kökeninizi yazın ve localhost kayıtlarını canlı ortama taşımayın. Kod parçaları Ekim 2026'da cors 2.8.6, django-cors-headers 4.9, Starlette CORSMiddleware, Laravel 12 ve 13, Spring Security 7.1, ASP.NET Core 10 ve Vite 8 belgeleri ve kaynak koduyla karşılaştırıldı.

Express (Node.js): cors ara katmanı​

Code:
// npm install cors
const express = require('express');
const cors = require('cors');

const app = express();
const allowedOrigins = ['https://app.example.com'];
if (process.env.NODE_ENV !== 'production') allowedOrigins.push('http://localhost:5173');

app.use(cors({
  origin: allowedOrigins,      // listedeki kökeni aynen döndürür, Vary: Origin ekler
  credentials: true,           // yalnızca çerez kullanıyorsanız
  methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'],
  allowedHeaders: ['Content-Type', 'Authorization'],
  exposedHeaders: ['Content-Disposition'],
  maxAge: 600,
}));
// kimlik doğrulama, hız sınırlama ve route'lar bu satırın altına
app.use ile bağlandığında ara katman tüm ön kontrol isteklerini kendisi 204 ile yanıtlar; eski rehberlerdeki app.options('*', cors()) satırına gerek kalmaz (Express 5 zaten yalın * yerine /{*splat} gibi adı olan bir joker (wildcard) ister). allowedHeaders verilmezse paket tarayıcının istediği başlıkları olduğu gibi geri yansıtır. Listede olmayan bir köken Access-Control-Allow-Origin başlığı almaz ve tarayıcı yanıtı engeller; route ise yine de çalışır.

Django: django-cors-headers​

Python:
# pip install django-cors-headers   (settings.py)
INSTALLED_APPS = [
    # ...
    "corsheaders",
]
MIDDLEWARE = [
    "corsheaders.middleware.CorsMiddleware",   # olabildiğince üstte
    "django.middleware.security.SecurityMiddleware",
    "django.contrib.sessions.middleware.SessionMiddleware",
    "django.middleware.common.CommonMiddleware",
    # ...
]
CORS_ALLOWED_ORIGINS = ["https://app.example.com", "http://localhost:5173"]
CORS_URLS_REGEX = r"^/api/.*$"
CORS_ALLOW_CREDENTIALS = True                       # oturum çerezi başka kökenden
CSRF_TRUSTED_ORIGINS = ["https://app.example.com"]  # çerezle POST/PUT/DELETE
CorsMiddleware, CommonMiddleware ve WhiteNoise ara katmanlarının üstünde olmalıdır; aksi hâlde APPEND_SLASH yönlendirmesi ya da erken üretilen başka bir yanıt CORS başlıkları olmadan çıkar. Varsayılan izinli başlıklar authorization, content-type ve x-csrftoken değerlerini zaten içerir; özel başlıklar için corsheaders.defaults.default_headers listesini genişletin. Django CSRF'yi CORS'tan ayrı denetler; CSRF_TRUSTED_ORIGINS bu yüzden burada da yer alıyor.

FastAPI (Starlette)​

Python:
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware

app = FastAPI()
app.add_middleware(
    CORSMiddleware,
    allow_origins=["https://app.example.com", "http://localhost:5173"],
    allow_credentials=True,
    allow_methods=["GET", "POST", "PUT", "PATCH", "DELETE"],
    allow_headers=["Authorization", "Content-Type"],
)
Starlette'in CORS ara katmanı varsayılan olarak yalnızca GET'e izin verir. Yöntemleri listeleyene kadar JSON POST'un ön kontrolü 400 durum kodu ve Disallowed CORS method gibi bir gövdeyle reddedilir. Bu gövde curl çıktısında göründüğü için hatayı bulmak kolaydır.

Laravel: config/cors.php​

HandleCors ara katmanı Laravel'in global middleware yığınında hazır gelir ve OPTIONS isteklerini yanıtlar. Laravel 11'den beri yapılandırma dosyası varsayılan olarak yayımlanmaz:
Bash:
php artisan config:publish cors
PHP:
// config/cors.php
return [
    'paths' => ['api/*', 'sanctum/csrf-cookie', 'login', 'logout'],
    'allowed_methods' => ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'],
    'allowed_origins' => ['https://app.example.com'],
    'allowed_origins_patterns' => [],
    'allowed_headers' => ['Content-Type', 'Authorization', 'X-Requested-With', 'X-XSRF-TOKEN'],
    'exposed_headers' => [],
    'max_age' => 600,
    'supports_credentials' => true,
];
Burada iki tuzak var. Birincisi, paths listesiyle eşleşmeyen bir route hiç CORS başlığı almaz. İkincisi, varsayılan dosya allowed_origins ['*'] değerini supports_credentials false ile birlikte getirir: supports_credentials değerini true yapıp '*' değerini bırakırsanız alttaki fruitcake/php-cors servisi gelen her Origin'i Allow-Credentials: true ile birlikte geri yansıtır. Kökenleri tek tek yazın. Canlıda yapılandırmayı önbelleğe alıyorsanız değişiklikten sonra php artisan config:cache komutunu yeniden çalıştırın.

Spring Boot ve Spring Security​

Code:
@Bean
SecurityFilterChain api(HttpSecurity http) throws Exception {
    http.cors(Customizer.withDefaults())   // CORS kimlik doğrulamadan önce çalışır
        .authorizeHttpRequests(auth -> auth.anyRequest().authenticated());
    return http.build();
}

@Bean
UrlBasedCorsConfigurationSource corsConfigurationSource() {
    CorsConfiguration config = new CorsConfiguration();
    config.setAllowedOrigins(List.of("https://app.example.com"));
    config.setAllowedMethods(List.of("GET", "POST", "PUT", "PATCH", "DELETE"));
    config.setAllowedHeaders(List.of("Authorization", "Content-Type"));
    config.setAllowCredentials(true);
    config.setMaxAge(600L);
    UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
    source.registerCorsConfiguration("/api/**", config);
    return source;
}
Spring Security belgeleri sıranın neden önemli olduğunu açıklar: ön kontrol isteği çerez taşımaz, bu yüzden önce çalışan bir güvenlik filtresi isteği kimliği doğrulanmamış sayıp reddeder. Spring Security kullanmıyorsanız controller üzerinde @CrossOrigin(origins = "https://app.example.com") ya da WebMvcConfigurer.addCorsMappings kuralı yeterlidir. allowedOrigins("*") ile allowCredentials(true) birlikte kullanılırsa Spring, allowedOriginPatterns kullanmanızı öneren bir IllegalArgumentException fırlatır; ancak allowedOriginPatterns("*") kimlik bilgisiyle her siteye kapıyı açar, bu yüzden açık bir liste tutun.

ASP.NET Core: AddCors ve UseCors sırası​

Code:
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddCors(options =>
{
    options.AddPolicy("Spa", policy => policy
        .WithOrigins("https://app.example.com", "http://localhost:5173")  // sonda eğik çizgi yok
        .WithMethods("GET", "POST", "PUT", "PATCH", "DELETE")
        .WithHeaders("Authorization", "Content-Type")
        .AllowCredentials());
});
builder.Services.AddControllers();

var app = builder.Build();
app.UseRouting();
app.UseCors("Spa");          // UseRouting'den sonra, kimlik doğrulamadan önce
app.UseAuthentication();
app.UseAuthorization();
app.MapControllers();
app.Run();
Microsoft belgeleri bu konuda net: UseCors, UseRouting'den sonra ve UseAuthorization'dan önce gelir; sıra UseCors, UseAuthentication, UseAuthorization şeklindedir ve kullanıyorsanız UseResponseCaching'den önce yer alır. Sonunda eğik çizgi olan bir köken hiçbir zaman eşleşmez. AllowAnyOrigin ile AllowCredentials birlikte geçersiz bir CORS yanıtı üretir; SetIsOriginAllowed(_ => true) ile AllowCredentials birleşimi ise belgelerde güvensiz olarak işaretlenir.

Nginx: map ile izin listesi​

Code:
# http {} bloğu: izin listesi
map $http_origin $cors_origin {
    default                                     "";
    "https://app.example.com"                   $http_origin;
    "~^https://([a-z0-9-]+\.)?example\.com$"    $http_origin;   # yalnızca tüm alt alan adları sizin kontrolünüzdeyse
}

server {
    # ... listen, server_name, TLS ...
    location /api/ {
        if ($request_method = OPTIONS) {
            add_header Access-Control-Allow-Origin $cors_origin always;
            add_header Access-Control-Allow-Methods "GET, POST, PUT, PATCH, DELETE" always;
            add_header Access-Control-Allow-Headers "Authorization, Content-Type" always;
            add_header Access-Control-Allow-Credentials "true" always;
            add_header Access-Control-Max-Age 600 always;
            add_header Vary Origin always;
            return 204;
        }
        add_header Access-Control-Allow-Origin $cors_origin always;
        add_header Access-Control-Allow-Credentials "true" always;
        add_header Vary Origin always;
        proxy_hide_header Access-Control-Allow-Origin;
        proxy_hide_header Access-Control-Allow-Credentials;
        proxy_pass http://127.0.0.1:8080;
    }
}
Map'te bulunmayan bir köken için $cors_origin boş kalır ve Nginx değeri boş olan add_header satırını atlar; böylece bilinmeyen siteler Allow-Origin almaz. Buradaki always parametresi şarttır: o olmadan Nginx başlıkları yalnızca 200, 201, 204, 206, 301, 302, 303, 304, 307 ve 308 yanıtlarına ekler ve 401, 404, 502 sayfalarınız CORS hatası olarak görünür. proxy_hide_header arka ucun gönderdiği kopyaları siler, böylece tarayıcı çift başlık görmez. Pek çok kişinin gözden kaçırdığı bir Nginx kuralı daha var: kendi add_header satırları olan bir location, server bloğundaki add_header satırlarını (HSTS, CSP vb.) artık devralmaz. Onları bu blokta tekrarlayın ya da Nginx 1.29.3 ve sonrasında add_header_inherit merge kullanın.

Apache: .htaccess veya VirtualHost​

Code:
# mod_setenvif ve mod_headers gerekir (mod_rewrite yalnızca OPTIONS yanıtı için)
SetEnvIf Origin "^https://(app|admin)\.example\.com$" CORS_ORIGIN=$0
Header always set Access-Control-Allow-Origin "%{CORS_ORIGIN}e" env=CORS_ORIGIN
Header always set Access-Control-Allow-Credentials "true" env=CORS_ORIGIN
Header always set Access-Control-Allow-Methods "GET, POST, PUT, PATCH, DELETE" env=CORS_ORIGIN
Header always set Access-Control-Allow-Headers "Authorization, Content-Type" env=CORS_ORIGIN
Header always merge Vary "Origin"

RewriteEngine On
RewriteCond %{REQUEST_METHOD} =OPTIONS
RewriteCond %{HTTP:Access-Control-Request-Method} .
RewriteRule ^ - [R=204,L]
SetEnvIf, Origin değerini yalnızca iki ucu sabitlenmiş desen eşleştiğinde CORS_ORIGIN değişkenine kopyalar ($0 eşleşmenin tamamıdır); env=CORS_ORIGIN koşulu da başlıkları yalnızca bu durumda ekler. always koşulu hata ve yönlendirme yanıtlarını da kapsar. Rewrite kuralı gerçek ön kontrol isteklerini 204 ile yanıtlar: 3xx dışında bir kod verildiğinde mod_rewrite hedef adresi yok sayar ve L bayrağı varmış gibi durur. Uygulamanız OPTIONS'ı zaten yanıtlıyorsa bu kısmı eklemeyin. Bu bloğu Apache 2.4.67 üzerinde curl ile denedik: izinli köken tüm başlıklarla 204 aldı, izinsiz köken ve benzer görünen https://app.example.com.evil.net hiçbir CORS başlığı almadı, 404 yanıtı da başlıkları taşıdı.

Kimlik bilgileri ve çerezler (credentials)​

Önce gerçekte ne gönderdiğinizi netleştirin. CORS açısından kimlik bilgisi (credentials) çerezler, HTTP kimlik doğrulaması ve TLS istemci sertifikaları demektir. Kodunuzun Authorization başlığına kendi eklediği Bearer belirteci (token) bu anlamda kimlik bilgisi sayılmaz: credentials: 'include' ya da Allow-Credentials gerektirmez, ama Authorization'ın Allow-Headers listesine açıkça yazılması gerekir ve istek ön kontrollü olur.
  • İstemci: fetch(url, { credentials: 'include' }); axios'ta withCredentials: true (istek başına ya da axios.defaults.withCredentials = true ile); XMLHttpRequest'te xhr.withCredentials = true. fetch'in varsayılan değeri 'same-origin' olduğundan çerezler siz istemedikçe başka bir kökene gitmez.
  • Sunucu: Access-Control-Allow-Credentials: true ve kökeni birebir döndüren bir Access-Control-Allow-Origin. Kimlik bilgili isteklerde * değeri Allow-Origin, Allow-Headers, Allow-Methods ve Expose-Headers başlıklarında joker anlamını yitirir (Allow-Origin'de tarayıcı yanıtı doğrudan reddeder).
  • Çerezler: farklı köken, farklı site demek değildir. app.example.com ile api.example.com farklı kökenlerdir ama aynı sitedir (aynı kayıt edilebilir alan adı ve şema); CORS kimlik bilgisine izin verdiğinde SameSite=Lax bir çerez de gönderilir. Kayıt edilebilir alan adı Public Suffix List'e göre belirlenir: github.io, vercel.app veya herokuapp.com gibi paylaşımlı platform alan adlarında iki alt alan adı farklı site sayılır. myapp.com'dan api.vendor.io'ya yapılan çağrı ise siteler arasıdır (cross-site): çerezin HTTPS üzerinden SameSite=None; Secure ile ayarlanması gerekir ve yine de üçüncü taraf çerez politikalarına takılabilir. Safari'nin izleme önleme özelliği üçüncü taraf çerezleri varsayılan olarak kısıtlar, Firefox her siteye ayrı bir çerez deposu (cookie jar) ayırır, Chrome ise gizli pencerede ya da kullanıcı isterse bunları engeller.
Pratik sonuç: çerez tabanlı oturum kullanan bir tek sayfa uygulamasında (SPA) API'yi aynı sitenin bir alt alan adında barındırın ya da aynı kökende /api yolu altında yayınlayın. Bir kökene kimlik bilgisi izni verdiğiniz anda o köken kullanıcı adına işlem yapabilir; bu yüzden CSRF korumasını açık tutun (Django'da CSRF_TRUSTED_ORIGINS, Laravel Sanctum'da XSRF-TOKEN akışı). Oturum çerezi ile belirteç arasında henüz karar vermediyseniz OAuth 2.0 ve OIDC rehberimiz akışları ve güvenlik açısından artılarını, eksilerini anlatıyor.

Geliştirme ortamında proxy ile tek köken​

En temiz geliştirme düzeni CORS'a hiç ihtiyaç duymaz: geliştirme sunucusu (dev server) /api altındaki her isteği arka uca iletir, tarayıcı da yalnızca tek bir kökenle konuşur. Vite ile:
Code:
// vite.config.js
import { defineConfig } from 'vite'

export default defineConfig({
  server: {
    proxy: {
      '/api': {
        target: 'http://localhost:8000',
        changeOrigin: true,
      },
    },
  },
})
Ön yüz artık http://localhost:8000/api/orders yerine fetch('/api/orders') çağırır. Yol olduğu gibi iletilir; arka uç /api önekini kullanmıyorsa Vite'in rewrite seçeneğiyle kaldırabilirsiniz. Angular CLI'da ng serve --proxy-config proxy.conf.json, Next.js'te next.config.js içindeki rewrites, webpack-dev-server'da devServer.proxy aynı işi görür. Canlı ortamda aynı fikri reverse proxy ile uygularsınız: istekleri arka uca ileten bir Nginx location /api/ bloğu uygulamayı ve API'yi tek kökende toplar.

Şunlar çözüm değildir:
  • mode: 'no-cors'. Yanıt opaktır (opaque response): durum kodu 0'dır, başlıklar ve gövde JavaScript'e kapalıdır. Yalnızca GET, HEAD ve POST'a izin verilir; tarayıcı Authorization ya da application/json değerli Content-Type gibi izinli liste dışındaki başlıkları sessizce düşürür. Yani sunucuya giden istek bile sizin yazdığınız istek değildir.
  • Tarayıcı güvenliğini kapatmak. --disable-web-security bayrağı ya da "CORS unblock" eklentileri yalnızca kendi tarayıcınızı değiştirir. Kullanıcılarınız hatayı görmeye devam eder; o tarayıcıda açtığınız diğer her site de aynı köken politikası (same-origin policy) olmadan çalışır.
  • Herkese açık CORS proxy'leri. Belirteçleriniz ve verileriniz üçüncü bir taraftan geçer. Herkese açık veriyle kısa bir deneme için kabul edilebilirler; canlı ortam için asla.

CORS hatasını tekrar tekrar getiren tuzaklar​

  • Ön kontrolü kimlik doğrulama, WAF ya da CDN engelliyor. OPTIONS isteği çerez ve Authorization taşımadığından önce çalışan kimlik doğrulama ara katmanı 401 döndürür. CORS'u en başa alın, WAF ve CDN kurallarında OPTIONS yöntemine izin verin ve testi kendi ağınızın dışından curl ile yapın.
  • Yönlendirmeler. http'den https'e geçiş, sondaki eğik çizgi yönlendirmesi (Django'da APPEND_SLASH, Laravel'in varsayılan .htaccess dosyasındaki 301 kuralı, FastAPI'nin 307 ile çalışan redirect_slashes ayarı) ya da giriş sayfasına yapılan 302 yönlendirmesi ön kontrolü doğrudan bozar. Basit isteklerde yönlendirme yanıtının kendisi de CORS başlığı taşımalıdır; köken değiştiren bir yönlendirmeden sonra Origin değeri null olur. Chrome mesajında (redirected from '...') ifadesini görüyorsanız sorun budur.
  • Çift başlık. Access-Control-Allow-Origin başlığını hem uygulama hem proxy eklerse Chrome multiple values hatası verir. CORS'u tek katmanda yapılandırın; Nginx'te arka ucun kopyasını proxy_hide_header ile kaldırın.
  • Vary: Origin olmadan önbellekleme. Başka bir köken için üretilmiş yanıtı tekrar sunan bir proxy ya da CDN, "not equal to the supplied origin" hatasına yol açar. Allow-Origin değiştiği her durumda Vary: Origin gönderin. Bazı CDN'ler Vary'yi önbellek anahtarına ancak ayarlandığında katar; Cloudflare'de bu iş Temmuz 2026'da eklenen Cache Rules içindeki Vary ayarıyla yapılır. Web fontları gibi kimlik bilgisi gerektirmeyen herkese açık dosyalarda en basit çözüm Access-Control-Allow-Origin: * olur.
  • file:// ile açılan sayfalar. Bu sayfaların Origin değeri null'dır. null kökenine asla izin vermeyin (sandbox özniteliği taşıyan iframe'ler ve bazı yönlendirmeler de null gönderir); sayfayı yerel bir web sunucusuyla açın, örneğin python3 -m http.server 8080.
  • Chrome'un yerel ağ erişimi kısıtlaması (Local Network Access). Chrome 142'den beri herkese açık bir sitenin özel bir IP'ye (192.168.x.x, 10.x.x.x) ya da localhost'a gönderdiği istekler kullanıcının iznine bağlıdır. İzin verilmezse konsolda "Permission was denied for this request to access the ... address space" yazar (noktaların yerinde local ya da loopback bulunur) ve CORS başlıkları tek başına bunu çözmez. Eski rehberlerdeki Access-Control-Allow-Private-Network başlığı, Chrome'un askıya aldığı Private Network Access önerisine aittir. Yerel ağ ve aynı cihaz (loopback) için ayrı izin istenip istenmediği gibi ayrıntılar Chrome sürümleri arasında hâlâ değişiyor; güncel davranışı Chrome belgelerinden kontrol edin.
  • Yalnızca fetch değil. @font-face ile yüklenen web fontları, type="module" betikler ve crossorigin özniteliğiyle canvas'a çizilen görseller de CORS denetiminden geçer.
  • CORS kimlik doğrulama değildir. API'nizi herhangi bir istemci çağırabilir; tarayıcı yanıtı sayfaya vermeyi reddetse bile sunucu basit istekleri işler. Kimlik doğrulama, yetkilendirme, CSRF koruması ve hız sınırlarına yine ihtiyacınız var.

Güvenlik kontrol listesi​

  • Her ortam için birebir eşleşen bir izin listesi (allow-list) tutun; canlı listede localhost bulunmasın.
  • Gelen Origin değerini denetlemeden Allow-Credentials: true ile birlikte asla geri yansıtmayın. Şu birleşimlerin her biri tam olarak bunu yapar: Laravel'de allowed_origins ['*'] ile supports_credentials true; FastAPI/Starlette'te allow_origins=["*"] ile allow_credentials=True; django-cors-headers paketinde CORS_ALLOW_ALL_ORIGINS ile CORS_ALLOW_CREDENTIALS; Express cors paketinde origin: true ile credentials: true; ASP.NET Core'da SetIsOriginAllowed(_ => true) ile AllowCredentials; Spring'de allowedOriginPatterns("*") ile allowCredentials(true).
  • Düzenli ifadeleri (regex) iki uçtan sabitleyin: ^https://(app|admin)\.example\.com$. /example\.com$/ deseni https://evilexample.com ile de eşleşir; startsWith('https://app.example.com') kontrolü https://app.example.com.evil.net adresini de kabul eder.
  • null kökenine izin vermeyin.
  • Yalnızca ön yüzün kullandığı yöntem ve başlıklara izin verin; yalnızca gereken yanıt başlıklarını dışarı açın (Expose-Headers).
  • Allow-Origin isteğe göre değişiyorsa her zaman Vary: Origin gönderin.
  • * değerini yalnızca kimlik bilgisi gerektirmeyen, gerçekten herkese açık kaynaklarda kullanın; kimlik doğrulama yerine ağ konumuna güvenen iç servislerde kullanmayın.
  • Çerez kullanıyorsanız CSRF korumasını açık tutun ve reddedilen kökenleri loglayın; yanlış yapılandırmalar böylece erkenden ortaya çıkar.
Konunun genel çerçevesi için pratik OWASP Top 10 rehberimize göz atabilirsiniz.

Sık sorulan sorular​

Postman ve curl ile çalışıyor, tarayıcıda neden çalışmıyor?
CORS'u yalnızca tarayıcılar uygular. Postman ve curl yanıtı CORS başlıklarına bakmadan gösterir; bu yüzden sunucunuzun doğru yapılandırılıp yapılandırılmadığı hakkında bir şey söylemezler.

CORS hatasını ön yüz kodundan düzeltebilir miyim?
Hayır. Ya çağrıyı bir geliştirme proxy'si ya da reverse proxy ile aynı kökene taşırsınız ya da sunucunun doğru başlıkları göndermesini sağlarsınız. mode: 'no-cors' size yalnızca okuyamayacağınız opak bir yanıt verir.

Access-Control-Allow-Origin: * güvenli mi?
Kimlik bilgisi gerektirmeyen herkese açık veriler için evet; zaten çerezle çalışmaz. Kimlik doğrulama yerine ağ konumuna güvenen iç servislerde kullanmayın: CORS açısından * değeri, kullanıcının ziyaret ettiği her sayfanın yanıtı okumasına izin verir; önünde yalnızca Chrome'un yerel ağ erişimi izni gibi ayrı korumalar kalır.

Bearer token için credentials: 'include' gerekir mi?
Hayır. Bu seçenek çerezleri, HTTP kimlik doğrulamasını ve istemci sertifikalarını kapsar. Kendi eklediğiniz bir belirteç için Allow-Headers listesine Authorization'ı açıkça yazın ve bir ön kontrol isteği bekleyin.

Fazladan giden OPTIONS isteklerini nasıl azaltırım?
Access-Control-Max-Age ile ön kontrol sonucunu tarayıcının önbelleğine aldırın (Chromium bu süreyi 7200, Firefox 86400 saniyeyle sınırlar). Önbellek adres (URL) bazında tutulur; /orders/1 ile /orders/2 ayrı kayıtlardır. Gereksiz özel başlıklardan vazgeçin; uygulamayı ve API'yi tek kökenden sunmak ön kontrolleri tamamen ortadan kaldırır.

Sonuç ve sonraki adımlar​

CORS hatası, tarayıcının "sunucu bu sayfanın kökenine izin vermedi" demesidir. Çalışma sırası hep aynıdır: konsol mesajını okuyun, DevTools'ta ön kontrol satırını bulun, curl ile yeniden üretin ve izni sunucuda, tek katmanda, birebir eşleşen bir izin listesiyle verin. Sıradaki adımlar: canlı ve geliştirme köken listelerinizi ayırın, hata yanıtlarının da CORS başlığı taşıdığını curl ile doğrulayın ve yerel geliştirmeyi Vite'in (ya da kullandığınız çatının) proxy'si arkasına alın. Ön yüzü ve API'si birlikte tasarlanması gereken kurumsal bir uygulama planlıyorsanız AIOR'un kurumsal ve endüstriyel yazılım hizmetinde yetkilendirme ve API entegrasyonları, kurulum ve bakımla birlikte tasarlanır. Kendi kurulumunuzda hata devam ederse konsoldaki mesajı olduğu gibi bu konuya yazın.

You open the browser console and find "has been blocked by CORS policy". Your front end at http://localhost:5173 is calling an API at http://localhost:8000, or https://app.example.com is calling https://api.example.com in production, and the very same request works in curl and Postman. That contrast explains the error: CORS (Cross-Origin Resource Sharing) is enforced by the browser, and the permission has to come from the server that answers the request. This guide shows how to read the error, find the missing header with DevTools and curl, and fix the CORS error properly in Express, Django, FastAPI, Laravel, Spring Boot, ASP.NET Core, Nginx and Apache, without unsafe shortcuts.

Short answer: the API server must send Access-Control-Allow-Origin with your page's exact origin, answer the OPTIONS preflight with a 2xx status that lists the methods and headers you use, and, if you send cookies, add Access-Control-Allow-Credentials: true together with an explicit origin instead of *.

The CORS error messages in Chrome and Firefox​

Chromium-based browsers such as Chrome and Edge print messages in this form. The text after "CORS policy:" tells you what failed:

Code:
Access to fetch at 'https://api.example.com/v1/orders' from origin 'https://app.example.com' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.

Access to XMLHttpRequest at 'https://api.example.com/v1/orders' from origin 'https://app.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 on the requested resource.

Access to fetch at 'https://api.example.com/v1/me' from origin 'https://app.example.com' has been blocked by CORS policy: The value of the 'Access-Control-Allow-Origin' header in the response must not be the wildcard '*' when the request's credentials mode is 'include'.

Firefox reports the same failures in its own words:

Code:
Cross-Origin Request Blocked: The Same Origin Policy disallows reading the remote resource at https://api.example.com/v1/orders. (Reason: CORS header ‘Access-Control-Allow-Origin’ missing). Status code: 200.

Cross-Origin Request Blocked: The Same Origin Policy disallows reading the remote resource at https://api.example.com/v1/orders. (Reason: CORS preflight response did not succeed). Status code: 401.

Cross-Origin Request Blocked: The Same Origin Policy disallows reading the remote resource at ‘https://api.example.com/v1/me’. (Reason: Credential is not supported if the CORS header ‘Access-Control-Allow-Origin’ is ‘*’).

Your JavaScript only receives a generic error: TypeError: Failed to fetch in Chrome, TypeError: NetworkError when attempting to fetch resource. in Firefox, or AxiosError: Network Error with axios. The browser hides the reason from scripts on purpose and prints it only to the console. Chrome 135 and earlier also appended "If an opaque response serves your needs, set the request's mode to 'no-cors'..." to the fetch version of the missing-header message; that suggestion does not solve anything (see the development section below).

What actually happens​

  • The browser sends the request (first a preflight, when one is required), receives the response and checks its CORS headers. If they don't allow your page's origin, your code never sees the response.
  • When a preflight fails, the real request is never sent. When a simple request fails the check, the server has already received and possibly executed it. CORS decides who may read a response, not who may send a request.
  • curl, Postman, mobile apps and server-to-server calls don't apply CORS at all, so "it works in Postman" proves nothing about your CORS setup.
  • The fix lives on the server, or on the reverse proxy in front of it, which opts in by sending Access-Control-* headers. No front-end setting can grant that permission.

Origin basics: scheme, host and port​

An origin is the triple scheme://host:port. Two URLs share an origin only when all three parts match; the path is irrelevant.

PageRequest goes toSame origin?Why
https://app.example.comhttps://app.example.com/api/ordersYesOnly the path differs
http://localhost:3000http://localhost:8000NoDifferent port
http://localhost:5173http://127.0.0.1:5173NoDifferent host string
http://example.comhttps://example.comNoDifferent scheme
https://www.example.comhttps://example.comNowww and the apex domain are different hosts
https://app.example.comhttps://api.example.comNoDifferent subdomain, though still the same site (see the cookies section)

The browser puts the page's origin in the Origin request header, with no path and no trailing slash. The server has to answer with exactly that string (or *) in Access-Control-Allow-Origin, so a configured value of https://app.example.com/ with a trailing slash never matches.

Simple requests vs preflighted requests​

A request counts as "simple" and goes straight out only if all of the following are true:
  • The method is GET, HEAD or POST.
  • Your code sets only CORS-safelisted headers: Accept, Accept-Language, Content-Language, Content-Type and a single-range Range header.
  • Content-Type, if present, is application/x-www-form-urlencoded, multipart/form-data or text/plain.
  • The body is not a ReadableStream, and no upload listeners are registered on XMLHttpRequest.
Everything else gets a CORS preflight request: before the real request, the browser sends an OPTIONS request carrying Origin, Access-Control-Request-Method and Access-Control-Request-Headers. In real applications the usual triggers are PUT, PATCH and DELETE, an Authorization: Bearer header, custom headers such as X-Requested-With or X-Tenant-Id, and Content-Type: application/json. In other words, almost every JSON POST to an API is preflighted. The preflight never carries cookies or an Authorization header, and its response must be a 2xx (200 or 204) with no redirect.

Request header (browser)Response header (server)Rule
OriginAccess-Control-Allow-OriginExactly one value: the requesting origin or *; * is rejected for credentialed requests
Access-Control-Request-Method (preflight)Access-Control-Allow-MethodsMust include the method; GET, HEAD and POST are always allowed
Access-Control-Request-Headers (preflight)Access-Control-Allow-HeadersMust list every requested header; * is a wildcard only for requests without credentials, and the Fetch standard excludes Authorization from it
Cookies, credentials: 'include'Access-Control-Allow-CredentialsThe only valid value is true, and it needs an explicit origin
(a script reads a response header)Access-Control-Expose-HeadersNeeded for anything beyond Cache-Control, Content-Language, Content-Length, Content-Type, Expires, Last-Modified and Pragma
(preflight caching)Access-Control-Max-AgeSeconds; Chromium caps it at 7200, Firefox at 86400; the default is 5
(proxies, CDNs, browser cache)Vary: OriginSend it whenever Allow-Origin depends on the request

Browsers lowercase and sort the names in Access-Control-Request-Headers (for example authorization,content-type). Header names are case-insensitive, so the server may list them in any case. One caveat on the * wildcard: Chrome and Firefox still let it cover Authorization on requests without credentials, but because the standard says otherwise, both show a deprecation warning. List Authorization by name.

Diagnose a CORS error in five steps​

  1. Find the right row in DevTools. Open the Network panel, choose the All filter and reproduce the error. Preflight rows belong to the Other category, so the Fetch/XHR filter hides them. In Chrome the failed call shows CORS error in the Status column, and hovering over it reveals the error code. Right-click a column header and add Method: a preflighted call then reads POST + Preflight, with a link to the OPTIONS row whose Type is preflight. Firefox lists the OPTIONS request as a separate row.
  2. Check the preflight status. It must be 2xx. A 301, 302, 307 or 308 means a redirect (Chrome says "Redirect is not allowed for a preflight request"); 401 or 403 means authentication or a WAF ran before CORS; 404 or 405 means nothing answers OPTIONS on that path; 5xx means the application failed.
  3. Read the response headers. Access-Control-Allow-Origin must match the page origin character for character and appear exactly once; Allow-Methods and Allow-Headers must cover what the browser asked for; Allow-Credentials must be true when cookies are involved.
  4. Reproduce it with curl, so you can test each server change without the browser (commands below).
  5. Check the error paths. 401, 404 and 500 responses must carry the CORS headers too. If they don't, the browser shows a CORS error instead of the real one.

Bash:
# 1) The preflight, exactly as the browser sends it
curl -i -X OPTIONS https://api.example.com/v1/orders \
  -H "Origin: https://app.example.com" \
  -H "Access-Control-Request-Method: POST" \
  -H "Access-Control-Request-Headers: authorization,content-type"

# 2) The real request and an error response must carry the headers too
curl -i https://api.example.com/v1/orders -H "Origin: https://app.example.com"
curl -i https://api.example.com/v1/no-such-route -H "Origin: https://app.example.com"

A healthy preflight response looks roughly like this (header order and letter case vary by server):

Code:
HTTP/2 204
access-control-allow-origin: https://app.example.com
access-control-allow-methods: GET, POST, PUT, PATCH, DELETE
access-control-allow-headers: Authorization, Content-Type
access-control-allow-credentials: true
access-control-max-age: 600
vary: Origin

If the 404 or 401 response lacks access-control-allow-origin, fix that path too: always in Nginx, Header always in Apache, and the CORS middleware ahead of the application's error handlers. For consistent error bodies, see our REST API design guide.

Quick reference: message, cause, fix​

The message containsLikely causeFix
No 'Access-Control-Allow-Origin' header is presentNo CORS configuration for this path or origin, or the response is an error page from the proxy or the appAdd the origin to the allow-list; add the headers to error responses
Response to preflight request doesn't pass access control checkOPTIONS got 401/403/404/405 or came back without headersAnswer OPTIONS before authentication, with 204 and the Allow-* headers
Redirect is not allowed for a preflight requesthttp to https, trailing slash or login redirectCall the final URL directly
must not be the wildcard '*' when the request's credentials mode is 'include'Credentialed request answered with Allow-Origin: *Echo the allowed origin and send Allow-Credentials: true
contains multiple values ..., but only one is allowedBoth the proxy and the app add the header, or several origins sit in one headerHandle CORS in one layer; send one origin per response
Method PUT is not allowed by Access-Control-Allow-MethodsThe method is missing from the preflight responseAdd it to the allowed methods
Request header field authorization is not allowed by Access-Control-Allow-HeadersThe header is missing from Allow-HeadersList it by name; with credentials, * is not a wildcard
has a value '...' that is not equal to the supplied originA hard-coded origin differs (www, port, http vs https, trailing slash)Match the origin string exactly

CORS configuration for Express, Django, Laravel, Nginx and more​

In each example, replace https://app.example.com with your front-end origin and keep localhost entries out of production. The snippets were checked in October 2026 against the documentation and source code of cors 2.8.6, django-cors-headers 4.9, Starlette's CORSMiddleware, Laravel 12 and 13, Spring Security 7.1, ASP.NET Core 10 and Vite 8.

Express (Node.js): the cors middleware​

Code:
// npm install cors
const express = require('express');
const cors = require('cors');

const app = express();
const allowedOrigins = ['https://app.example.com'];
if (process.env.NODE_ENV !== 'production') allowedOrigins.push('http://localhost:5173');

app.use(cors({
  origin: allowedOrigins,      // echoes a listed origin and adds Vary: Origin
  credentials: true,           // only if you use cookies
  methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'],
  allowedHeaders: ['Content-Type', 'Authorization'],
  exposedHeaders: ['Content-Disposition'],
  maxAge: 600,
}));
// authentication, rate limiting and routes go below this line
Mounted with app.use, the middleware answers every preflight itself with 204, so the app.options('*', cors()) line from older tutorials is unnecessary (Express 5 wants a named wildcard such as /{*splat} instead of a bare * anyway). Without allowedHeaders the package echoes whatever headers the browser requests. An origin that isn't on the list gets no Access-Control-Allow-Origin header, so the browser blocks the response, but the route still runs.

Django: django-cors-headers​

Python:
# pip install django-cors-headers   (settings.py)
INSTALLED_APPS = [
    # ...
    "corsheaders",
]
MIDDLEWARE = [
    "corsheaders.middleware.CorsMiddleware",   # as high as possible
    "django.middleware.security.SecurityMiddleware",
    "django.contrib.sessions.middleware.SessionMiddleware",
    "django.middleware.common.CommonMiddleware",
    # ...
]
CORS_ALLOWED_ORIGINS = ["https://app.example.com", "http://localhost:5173"]
CORS_URLS_REGEX = r"^/api/.*$"
CORS_ALLOW_CREDENTIALS = True                       # session cookie across origins
CSRF_TRUSTED_ORIGINS = ["https://app.example.com"]  # unsafe methods with cookies
CorsMiddleware must come before CommonMiddleware and WhiteNoise; otherwise an APPEND_SLASH redirect or another early response leaves without CORS headers. The default allowed headers already include authorization, content-type and x-csrftoken, and you can extend corsheaders.defaults.default_headers for custom ones. Django checks CSRF separately from CORS, which is why CSRF_TRUSTED_ORIGINS appears here too.

FastAPI (Starlette)​

Python:
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware

app = FastAPI()
app.add_middleware(
    CORSMiddleware,
    allow_origins=["https://app.example.com", "http://localhost:5173"],
    allow_credentials=True,
    allow_methods=["GET", "POST", "PUT", "PATCH", "DELETE"],
    allow_headers=["Authorization", "Content-Type"],
)
Starlette's middleware allows only GET by default, so until you list your methods, a JSON POST preflight is rejected with status 400 and a body such as Disallowed CORS method. That body shows up in curl output, which makes this middleware easy to debug.

Laravel: config/cors.php​

The HandleCors middleware is already in Laravel's global middleware stack and answers OPTIONS requests. Since Laravel 11 the configuration file isn't published by default:
Bash:
php artisan config:publish cors
PHP:
// config/cors.php
return [
    'paths' => ['api/*', 'sanctum/csrf-cookie', 'login', 'logout'],
    'allowed_methods' => ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'],
    'allowed_origins' => ['https://app.example.com'],
    'allowed_origins_patterns' => [],
    'allowed_headers' => ['Content-Type', 'Authorization', 'X-Requested-With', 'X-XSRF-TOKEN'],
    'exposed_headers' => [],
    'max_age' => 600,
    'supports_credentials' => true,
];
Two traps. A route that doesn't match paths gets no CORS headers at all. And the shipped defaults pair allowed_origins ['*'] with supports_credentials false: if you turn credentials on and leave '*' in place, the underlying fruitcake/php-cors service echoes any Origin back together with Allow-Credentials: true. List your origins. If production caches its configuration, run php artisan config:cache again after editing.

Spring Boot with Spring Security​

Code:
@Bean
SecurityFilterChain api(HttpSecurity http) throws Exception {
    http.cors(Customizer.withDefaults())   // CORS runs before authentication
        .authorizeHttpRequests(auth -> auth.anyRequest().authenticated());
    return http.build();
}

@Bean
UrlBasedCorsConfigurationSource corsConfigurationSource() {
    CorsConfiguration config = new CorsConfiguration();
    config.setAllowedOrigins(List.of("https://app.example.com"));
    config.setAllowedMethods(List.of("GET", "POST", "PUT", "PATCH", "DELETE"));
    config.setAllowedHeaders(List.of("Authorization", "Content-Type"));
    config.setAllowCredentials(true);
    config.setMaxAge(600L);
    UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
    source.registerCorsConfiguration("/api/**", config);
    return source;
}
The Spring Security reference explains why the order matters: the preflight carries no cookies, so a security filter that runs first rejects it as unauthenticated. Without Spring Security, @CrossOrigin(origins = "https://app.example.com") on a controller or a WebMvcConfigurer.addCorsMappings rule is enough. allowedOrigins("*") combined with allowCredentials(true) throws an IllegalArgumentException that suggests allowedOriginPatterns instead; but allowedOriginPatterns("*") would admit every site with credentials, so keep an explicit list.

ASP.NET Core: AddCors and the UseCors order​

Code:
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddCors(options =>
{
    options.AddPolicy("Spa", policy => policy
        .WithOrigins("https://app.example.com", "http://localhost:5173")  // no trailing slash
        .WithMethods("GET", "POST", "PUT", "PATCH", "DELETE")
        .WithHeaders("Authorization", "Content-Type")
        .AllowCredentials());
});
builder.Services.AddControllers();

var app = builder.Build();
app.UseRouting();
app.UseCors("Spa");          // after UseRouting, before authentication
app.UseAuthentication();
app.UseAuthorization();
app.MapControllers();
app.Run();
Microsoft's documentation is explicit: UseCors goes after UseRouting and before UseAuthorization, in the order UseCors, UseAuthentication, UseAuthorization, and before UseResponseCaching if you use it. An origin written with a trailing slash never matches. AllowAnyOrigin together with AllowCredentials produces an invalid CORS response, and SetIsOriginAllowed(_ => true) with AllowCredentials is flagged as insecure.

Nginx: map-based allow-list​

Code:
# http {} context: the allow-list
map $http_origin $cors_origin {
    default                                     "";
    "https://app.example.com"                   $http_origin;
    "~^https://([a-z0-9-]+\.)?example\.com$"    $http_origin;   # only if you control every subdomain
}

server {
    # ... listen, server_name, TLS ...
    location /api/ {
        if ($request_method = OPTIONS) {
            add_header Access-Control-Allow-Origin $cors_origin always;
            add_header Access-Control-Allow-Methods "GET, POST, PUT, PATCH, DELETE" always;
            add_header Access-Control-Allow-Headers "Authorization, Content-Type" always;
            add_header Access-Control-Allow-Credentials "true" always;
            add_header Access-Control-Max-Age 600 always;
            add_header Vary Origin always;
            return 204;
        }
        add_header Access-Control-Allow-Origin $cors_origin always;
        add_header Access-Control-Allow-Credentials "true" always;
        add_header Vary Origin always;
        proxy_hide_header Access-Control-Allow-Origin;
        proxy_hide_header Access-Control-Allow-Credentials;
        proxy_pass http://127.0.0.1:8080;
    }
}
An origin missing from the map leaves $cors_origin empty, and nginx skips an add_header whose value is empty, so unknown sites get no Allow-Origin. The always flag is essential: without it nginx adds headers only to 200, 201, 204, 206, 301, 302, 303, 304, 307 and 308 responses, and your 401, 404 and 502 pages surface as CORS errors. proxy_hide_header drops the back end's copies so the browser never sees duplicates. One nginx rule trips up many people: a location with its own add_header lines stops inheriting add_header from the server block (HSTS, CSP and the like). Repeat them there, or on nginx 1.29.3 and later use add_header_inherit merge.

Apache: .htaccess or VirtualHost​

Code:
# needs mod_setenvif and mod_headers (mod_rewrite only for the OPTIONS answer)
SetEnvIf Origin "^https://(app|admin)\.example\.com$" CORS_ORIGIN=$0
Header always set Access-Control-Allow-Origin "%{CORS_ORIGIN}e" env=CORS_ORIGIN
Header always set Access-Control-Allow-Credentials "true" env=CORS_ORIGIN
Header always set Access-Control-Allow-Methods "GET, POST, PUT, PATCH, DELETE" env=CORS_ORIGIN
Header always set Access-Control-Allow-Headers "Authorization, Content-Type" env=CORS_ORIGIN
Header always merge Vary "Origin"

RewriteEngine On
RewriteCond %{REQUEST_METHOD} =OPTIONS
RewriteCond %{HTTP:Access-Control-Request-Method} .
RewriteRule ^ - [R=204,L]
SetEnvIf copies the Origin into CORS_ORIGIN only when the anchored pattern matches ($0 is the whole match), and env=CORS_ORIGIN emits the headers only in that case. The always condition also covers error and redirect responses. The rewrite rule answers genuine preflights with 204: given a non-3xx code, mod_rewrite drops the substitution and stops as if L were set. Leave it out if your application already handles OPTIONS. We tried this block on Apache 2.4.67 with curl: the allowed origin got 204 with all headers, a disallowed origin and the look-alike https://app.example.com.evil.net got none, and a 404 still carried the headers.

Credentials and cookies​

Start by deciding what you actually send. For CORS, credentials means cookies, HTTP authentication and TLS client certificates. A Bearer token that your own code puts in an Authorization header is not a credential in this sense: it needs neither credentials: 'include' nor Allow-Credentials, but Authorization must be listed explicitly in Allow-Headers, and it makes the request preflighted.
  • Client: fetch(url, { credentials: 'include' }); in axios, withCredentials: true (per request or through axios.defaults.withCredentials = true); in XMLHttpRequest, xhr.withCredentials = true. fetch defaults to 'same-origin', so cookies never travel to another origin unless you ask.
  • Server: Access-Control-Allow-Credentials: true plus an Access-Control-Allow-Origin that echoes the exact origin. With credentials, * loses its wildcard meaning in Allow-Origin, Allow-Headers, Allow-Methods and Expose-Headers (in Allow-Origin it makes the browser reject the response outright).
  • Cookies: cross-origin is not the same as cross-site. app.example.com and api.example.com are different origins but the same site (same registrable domain and scheme), so once CORS allows credentials, a SameSite=Lax cookie is sent. The registrable domain is determined by the Public Suffix List, so on shared platform domains such as github.io, vercel.app or herokuapp.com, two subdomains count as different sites. A call from myapp.com to api.vendor.io is cross-site: the cookie needs SameSite=None; Secure over HTTPS, and it is still subject to third-party cookie policy. Safari's tracking prevention restricts third-party cookies by default, Firefox gives each site its own cookie jar, and Chrome blocks them in Incognito or when users choose to.
The practical takeaway: for a single-page app (SPA) that logs in with cookies, host the API on a subdomain of the same site or serve it under the same origin at /api. Once you allow credentials for an origin, that origin can act on the user's behalf, so keep CSRF protection on (CSRF_TRUSTED_ORIGINS in Django, the XSRF-TOKEN flow in Laravel Sanctum). If you are still choosing between session cookies and tokens, our OAuth 2.0 and OIDC guide walks through the flows and their security trade-offs.

In development: one origin through a proxy​

The cleanest development setup needs no CORS at all: the dev server forwards everything under /api to the back end, so the browser only ever talks to one origin. With Vite:
Code:
// vite.config.js
import { defineConfig } from 'vite'

export default defineConfig({
  server: {
    proxy: {
      '/api': {
        target: 'http://localhost:8000',
        changeOrigin: true,
      },
    },
  },
})
The front end then calls fetch('/api/orders') instead of http://localhost:8000/api/orders. The path is forwarded unchanged, and Vite's rewrite option can strip the prefix if the back end doesn't use it. Angular CLI (ng serve --proxy-config proxy.conf.json), Next.js (rewrites in next.config.js) and webpack-dev-server (devServer.proxy) do the same job. In production the same idea works with a reverse proxy: an Nginx location /api/ block that forwards to the back end puts the app and the API on one origin.

These are not fixes:
  • mode: 'no-cors'. The response is opaque: status 0, with headers and body hidden from JavaScript. Only GET, HEAD and POST are allowed, and the browser silently drops headers outside the safelist, such as Authorization or a Content-Type of application/json, so the server doesn't even receive the request you wrote.
  • Disabling browser security. The --disable-web-security flag or "CORS unblock" extensions change only your own browser. Your users still get the error, and every other site you open in that browser runs without the same-origin policy.
  • Public CORS proxies. Your tokens and data pass through a third party. They are acceptable for a quick demo with public data, never for production.

Gotchas that keep CORS errors coming back​

  • Authentication, a WAF or the CDN blocks the preflight. OPTIONS carries no cookies or Authorization, so authentication middleware that runs first returns 401. Put CORS first, allow the OPTIONS method in WAF and CDN rules, and test with curl from outside your network.
  • Redirects. An http to https hop, a trailing-slash redirect (Django's APPEND_SLASH, the 301 rule in Laravel's default .htaccess, FastAPI's redirect_slashes with a 307) or a 302 to a login page breaks the preflight outright. For simple requests the redirect response itself needs CORS headers, and after a cross-origin redirect the Origin becomes null. If Chrome's message contains (redirected from '...'), this is your problem.
  • Duplicate headers. When both the app and the proxy add Access-Control-Allow-Origin, Chrome reports multiple values. Configure CORS in one layer, and in Nginx strip the back end's copy with proxy_hide_header.
  • Caching without Vary: Origin. A proxy or CDN that replays a response generated for another origin causes "not equal to the supplied origin". Send Vary: Origin whenever Allow-Origin varies. Some CDNs add Vary to the cache key only when told to; on Cloudflare that is the Vary setting in Cache Rules, added in July 2026. For public files that need no credentials, such as web fonts, Access-Control-Allow-Origin: * is the simplest answer.
  • Pages opened from file://. Their Origin is null. Never allow null (sandboxed iframes and some redirects send it too); serve the page from a local web server instead, for example python3 -m http.server 8080.
  • Chrome's Local Network Access restrictions. Since Chrome 142, a public website that sends requests to a private IP (192.168.x.x, 10.x.x.x) or to localhost needs the user's permission. If it is denied, the console reports "Permission was denied for this request to access the ... address space", with local or loopback in place of the dots, and CORS headers alone won't fix it. The Access-Control-Allow-Private-Network header in older guides belongs to the earlier Private Network Access proposal, which Chrome put on hold. Details such as whether local network and loopback (apps on the same device) are separate permissions are still changing between Chrome versions, so check Chrome's current documentation.
  • It isn't just fetch. Web fonts loaded through @font-face, type="module" scripts and images drawn onto a canvas with the crossorigin attribute go through CORS as well.
  • CORS is not authentication. Any client can call your API, and the server processes simple requests even when the browser refuses to hand the response to the page. You still need authentication, authorization, CSRF protection and rate limits.

Security checklist​

  • Keep an exact-match allow-list per environment, and keep localhost out of the production list.
  • Never echo an arbitrary Origin together with Allow-Credentials: true. Each of these combinations does exactly that: Laravel allowed_origins ['*'] with supports_credentials true; FastAPI/Starlette allow_origins=["*"] with allow_credentials=True; django-cors-headers CORS_ALLOW_ALL_ORIGINS with CORS_ALLOW_CREDENTIALS; the Express cors package with origin: true and credentials: true; ASP.NET Core SetIsOriginAllowed(_ => true) with AllowCredentials; Spring allowedOriginPatterns("*") with allowCredentials(true).
  • Anchor regular expressions at both ends: ^https://(app|admin)\.example\.com$. The pattern /example\.com$/ also matches https://evilexample.com, and a startsWith('https://app.example.com') check accepts https://app.example.com.evil.net.
  • Never allow the null origin.
  • Allow only the methods and headers your front end uses, and expose only the response headers it needs.
  • Send Vary: Origin whenever Allow-Origin depends on the request.
  • Use * only for genuinely public resources that need no credentials, never for internal services that rely on network location instead of authentication.
  • With cookies, keep CSRF protection enabled, and log rejected origins so misconfigurations show up early.
For the wider picture, see our practical OWASP Top 10 guide.

FAQ​

Why does it work in Postman and curl but not in the browser?
Only browsers enforce CORS. Postman and curl show the response without looking at the CORS headers, so they say nothing about whether your server is configured correctly.

Can I fix a CORS error in front-end code?
No. Either move the call onto the same origin with a dev-server proxy or a reverse proxy, or have the server send the right headers. mode: 'no-cors' only gives you an opaque response you cannot read.

Is Access-Control-Allow-Origin: * safe?
For public data that needs no credentials, yes, and it doesn't work with cookies anyway. Avoid it on internal services that trust network location rather than authentication: as far as CORS is concerned, * lets any page the user visits read the response, and only separate protections such as Chrome's Local Network Access prompt stand in the way.

Do I need credentials: 'include' for a Bearer token?
No. That option covers cookies, HTTP authentication and client certificates. For a token you add yourself, list Authorization explicitly in Allow-Headers and expect a preflight.

How do I cut down the extra OPTIONS requests?
Let the browser cache the preflight with Access-Control-Max-Age (Chromium caps it at 7200 seconds, Firefox at 86400). The cache is per URL, so /orders/1 and /orders/2 are separate entries. Drop custom headers you don't need; serving the app and the API from one origin removes preflights entirely.

Conclusion and next steps​

A CORS error is the browser telling you that the server hasn't allowed your page's origin. The order of work is always the same: read the console message, find the preflight row in DevTools, reproduce it with curl, then grant permission on the server, in one layer, with an exact-match allow-list. Next, split your production and development origin lists, confirm with curl that error responses carry CORS headers, and move local development behind the Vite (or framework) proxy. If you are planning an internal business application whose front end and API have to be designed together, AIOR's enterprise and industrial software service designs permissions and API integrations together with deployment and maintenance. If your stack still throws an error, post the exact console message in this thread.
 

Forum statistics

Threads
925
Messages
932
Members
26
Latest member
Jessicawatson

Members online

No members online now.

Featured content

AIOR
AIOR TECHNOLOGY

Get a quote for all your needs

Hosting · Domain · Servers · Design · Software · Engineering · Industry Solutions

Get a quote

24/7 Support · Quick response

Back
Top