Pernah kena error Access to fetch at 'https://api.example.com' from origin 'http://localhost:3000' has been blocked by CORS policy waktu lagi develop frontend yang manggil API beda domain? Kalau iya, kamu nggak sendirian. CORS error adalah salah satu masalah paling sering muncul di web development, terutama saat project mulai pakai architecture microservice atau frontend terpisah dari backend.
Saya sendiri dulu pertama kali ketemu CORS waktu bikin React app yang manggil REST API dari CodeIgniter 4. Keduanya jalan di port berbeda di localhost. Hasilnya? Browser nolak mentah-mentah request-nya. Setelah berjam-jam debug, ternyata masalahnya bukan di code logic, tapi di header HTTP yang belum diset dengan benar di sisi server.
Di tutorial ini, saya akan jelaskan apa itu CORS sebenarnya (bukan sekadar "tambah header doang"), kenapa browser memblokir request, dan bagaimana cara fix-nya di berbagai stack: PHP native, CodeIgniter 4, Laravel, Node.js Express, dan juga di level Nginx. Plus, ada troubleshooting checklist kalau kamu sudah coba semua tapi masih gagal.
Apa Itu CORS dan Kenapa Browser Memblokir Request
CORS singkatan dari Cross-Origin Resource Sharing. Ini adalah mekanisme keamanan yang diterapkan browser untuk mengontrol apakah sebuah web page boleh mengakses resource dari domain, port, atau protocol yang berbeda.
Coba pikirkan begini: kamu punya website bank di https://bank-kamu.com. Tanpa CORS, website jahat di https://malicious-site.com bisa bikin JavaScript yang diam-diam mengirim request ke https://bank-kamu.com/api/transfer menggunakan cookie session kamu. Kalau browser nggak punya CORS, uang kamu bisa pindah tanpa sepengetahuan.
Browser mengecek CORS dengan cara mengirim preflight request menggunakan method OPTIONS. Server harus merespons dengan header yang tepat, baru browser mengizinkan request yang sebenarnya. Header kuncinya adalah Access-Control-Allow-Origin.
// Browser secara otomatis mengirim preflight OPTIONS request
// sebelum request utama kalau ada condition ini:
// 1. Method bukan GET, HEAD, atau POST
// 2. Ada custom header (Authorization, Content-Type: application/json, dll)
// 3. Request dikirim dengan credentials (cookies)
fetch('https://api.example.com/data', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer token123'
},
body: JSON.stringify({ name: 'test' })
})
.then(response => response.json())
.catch(err => console.error('CORS Error:', err));
Perlu dicatat, CORS hanya berlaku di browser. Kalau kamu pakai curl, Postman, atau server-to-server request, CORS tidak berpengaruh. Ini murni fitur keamanan browser.
Cara Mengatasi CORS di PHP Native
Kalau kamu pakai PHP tanpa framework, fix-nya cukup simpel. Tambahkan header CORS sebelum output apapun dikirim. Letakkan kode ini di awal file PHP yang menangani API request:
<?php
// Set header CORS sebelum output apapun
header('Access-Control-Allow-Origin: http://localhost:3000');
header('Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type, Authorization, X-Requested-With');
header('Access-Control-Allow-Credentials: true');
header('Access-Control-Max-Age: 86400'); // Cache preflight 24 jam
// Handle preflight OPTIONS request
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
http_response_code(200);
exit();
}
// Lanjutkan dengan logic API kamu
header('Content-Type: application/json');
echo json_encode(['status' => 'ok', 'message' => 'CORS berhasil!']);
Kalau kamu mau mengizinkan SEMUA origin (hanya untuk development, jangan di production):
<?php
// DEVELOPMENT ONLY - Jangan pakai ini di production!
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type, Authorization');
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
http_response_code(200);
exit();
}
Kenapa harus handle OPTIONS request? Karena browser mengirim preflight OPTIONS SEBELUM request utama. Kalau server tidak merespons OPTIONS dengan benar, browser langsung blokir dan request utama tidak pernah terkirim. Ini penyebab paling sering kenapa CORS masih error meskipun header sudah ditambahkan.
Cara Mengatasi CORS di CodeIgniter 4
Di CI4, ada beberapa cara. Yang paling clean adalah pakai Filter. Buat file baru di app/Filters/CorsFilter.php:
<?php
namespace App\Filters;
use CodeIgniter\HTTP\RequestInterface;
use CodeIgniter\HTTP\ResponseInterface;
use CodeIgniter\Filters\FilterInterface;
class CorsFilter implements FilterInterface
{
public function before(RequestInterface $request, $arguments = null)
{
// Set CORS headers
$response = service('response');
$response->setHeader('Access-Control-Allow-Origin', 'http://localhost:3000');
$response->setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
$response->setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization, X-Requested-With');
$response->setHeader('Access-Control-Allow-Credentials', 'true');
$response->setHeader('Access-Control-Max-Age', '86400');
// Handle preflight
if ($request->getMethod() === 'options') {
$response->setStatusCode(200);
return $response;
}
}
public function after(RequestInterface $request, ResponseInterface $response, $arguments = null)
{
// Tidak perlu apa-apa di sini
}
}
Lalu daftarkan filter di app/Config/Filters.php dan app/Config/Routes.php:
// app/Config/Filters.php - tambahkan alias
public array $aliases = [
'cors' => \App\Filters\CorsFilter::class,
// ... filter lainnya
];
// app/Config/Routes.php - terapkan ke group API
$routes->group('api', ['filter' => 'cors'], function ($routes) {
$routes->get('users', 'ApiController::users');
$routes->post('users', 'ApiController::createUser');
});
Untuk lebih lengkap tentang bikin REST API di CI4, cek tutorial Bangun REST API dengan CodeIgniter 4 yang sudah saya tulis sebelumnya.
Cara Mengatasi CORS di Laravel
Laravel 11 sudah punya CORS middleware bawaan. Tinggal konfigurasi di config/cors.php:
<?php
// config/cors.php
return [
'paths' => ['api/*', 'sanctum/csrf-cookie'],
'allowed_methods' => ['*'],
'allowed_origins' => ['http://localhost:3000', 'https://frontend-kamu.com'],
'allowed_origins_patterns' => [],
'allowed_headers' => ['*'],
'exposed_headers' => [],
'max_age' => 86400,
'supports_credentials' => true,
];
Kalau kamu pakai Laravel lama (sebelum v9) atau butuh custom logic, kamu bisa bikin middleware sendiri:
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
class CorsMiddleware
{
public function handle(Request $request, Closure $next)
{
$response = $request->getMethod() === 'OPTIONS'
? response('', 200)
: $next($request);
$response->headers->set('Access-Control-Allow-Origin', 'http://localhost:3000');
$response->headers->set('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
$response->headers->set('Access-Control-Allow-Headers', 'Content-Type, Authorization');
$response->headers->set('Access-Control-Allow-Credentials', 'true');
return $response;
}
}
Kalau kamu tertarik mendalami Laravel lebih lanjut, ada artikel tentang Struktur Project Laravel 11 dan Tips Trik Laravel Eloquent ORM.
Cara Mengatasi CORS di Node.js Express
Di Express, cara paling praktis adalah pakai package cors:
const express = require('express');
const cors = require('cors');
const app = express();
// Opsi 1: Izinkan semua origin (development only)
app.use(cors());
// Opsi 2: Konfigurasi spesifik (production-ready)
app.use(cors({
origin: ['http://localhost:3000', 'https://frontend-kamu.com'],
methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
allowedHeaders: ['Content-Type', 'Authorization'],
credentials: true,
maxAge: 86400
}));
// Opsi 3: Dynamic origin (cek database atau config)
const allowedOrigins = ['http://localhost:3000', 'https://app.example.com'];
app.use(cors({
origin: function (origin, callback) {
if (!origin || allowedOrigins.includes(origin)) {
callback(null, true);
} else {
callback(new Error('Not allowed by CORS'));
}
},
credentials: true
}));
app.get('/api/data', (req, res) => {
res.json({ message: 'CORS berhasil di Express!' });
});
app.listen(3001, () => console.log('API running on port 3001'));
Tanpa package, kamu bisa set header manual:
app.use((req, res, next) => {
res.header('Access-Control-Allow-Origin', 'http://localhost:3000');
res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
res.header('Access-Control-Allow-Headers', 'Content-Type, Authorization');
if (req.method === 'OPTIONS') {
return res.sendStatus(200);
}
next();
});
Tutorial tentang async/await di JavaScript bisa membantu kamu handle CORS fetch request dengan lebih clean. Cek artikel JavaScript Async Await dan Promise.
Cara Mengatasi CORS di Level Nginx
Kalau kamu pakai Nginx sebagai reverse proxy, set header CORS langsung di config Nginx. Ini berguna kalau kamu punya banyak aplikasi di belakang satu Nginx:
server {
listen 443 ssl;
server_name api.example.com;
location /api/ {
# CORS headers
add_header 'Access-Control-Allow-Origin' 'https://frontend-kamu.com' always;
add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, OPTIONS' always;
add_header 'Access-Control-Allow-Headers' 'Content-Type, Authorization, X-Requested-With' always;
add_header 'Access-Control-Allow-Credentials' 'true' always;
add_header 'Access-Control-Max-Age' 86400 always;
# Handle preflight OPTIONS
if ($request_method = 'OPTIONS') {
add_header 'Access-Control-Allow-Origin' 'https://frontend-kamu.com' always;
add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, OPTIONS' always;
add_header 'Access-Control-Allow-Headers' 'Content-Type, Authorization, X-Requested-With' always;
add_header 'Access-Control-Max-Age' 86400 always;
add_header 'Content-Type' 'text/plain charset=UTF-8';
add_header 'Content-Length' 0;
return 204;
}
proxy_pass http://127.0.0.1:3001;
}
}
Penting: Parameter always di belakang add_header memastikan header tetap dikirim meskipun response code bukan 200. Tanpa always, header CORS tidak muncul di error responses (404, 500), yang bikin debugging CORS makin susah.
Kalau kamu sering bermasalah dengan Nginx, baca juga tutorial Cara Debug Nginx 502 dan 504.
Troubleshooting CORS - Checklist Kalau Masih Gagal
Sudah tambah header tapi masih error? Ini checklist yang wajib kamu cek:
1. Cek Preflight OPTIONS Response
Buka DevTools (F12) -> tab Network -> cari request OPTIONS. Kalau OPTIONS gagal atau tidak ada header CORS di response-nya, request utama tidak akan pernah terkirim.
# Test preflight dengan curl
curl -X OPTIONS https://api.example.com/data \
-H "Origin: http://localhost:3000" \
-H "Access-Control-Request-Method: POST" \
-H "Access-Control-Request-Headers: Content-Type, Authorization" \
-v
# Cek response headers - harus ada Access-Control-Allow-Origin
2. Pastikan Origin Tepat Sama
CORS sangat ketat soal origin. Perbedaan kecil ini bikin request ditolak:
http://localhost:3000vshttp://localhost:3000/(trailing slash)http://localhost:3000vshttps://localhost:3000(protocol beda)http://localhost:3000vshttp://127.0.0.1:3000(hostname beda)http://example.comvshttp://www.example.com(www vs non-www)
3. Cek Header yang Dibutuhkan
Kalau request kamu pakai Authorization header atau Content-Type: application/json, server HARUS mengizinkan header tersebut di Access-Control-Allow-Headers:
# Cek header yang diminta browser
curl -X OPTIONS https://api.example.com/data \
-H "Origin: http://localhost:3000" \
-H "Access-Control-Request-Headers: Authorization, Content-Type" \
-v 2>&1 | grep "Access-Control-Allow-Headers"
# Response harus mengandung "Authorization" dan "Content-Type"
4. Cek urutan Header di PHP
Di PHP, semua header() harus dipanggil SEBELUM ada output apapun (termasuk whitespace, BOM, atau echo). Satu karakter output sebelum header = header tidak terkirim:
<?php
// BAHAYA: ada whitespace sebelum <?php di baris 1!
// atau ada BOM character di awal file
// Pastikan tidak ada output sebelum header()
// Cek dengan: headers_sent($file, $line)
if (headers_sent($file, $line)) {
die("Headers already sent in $file on line $line");
}
header('Access-Control-Allow-Origin: http://localhost:3000');
5. Cek Proxy dan CDN
Kalau kamu pakai Cloudflare, Nginx reverse proxy, atau CDN lain, mereka kadang strip header CORS. Pastikan header sampai ke client:
# Cek langsung dari server (bypass proxy)
curl -sI http://localhost/api/data | grep -i "access-control"
# Cek dari luar (lewati CDN/proxy)
curl -sI https://api.example.com/data | grep -i "access-control"
# Kalau hasilnya beda, berarti ada proxy yang strip header
Kesalahan Umum yang Sering Terjadi
Ini beberapa gotcha yang sering bikin developer frustasi:
- Menggunakan wildcard (*) dengan credentials:
Access-Control-Allow-Origin: *tidak bisa dipakai bersamaan denganAccess-Control-Allow-Credentials: true. Browser akan menolak. Harus set origin spesifik. - Lupa handle OPTIONS: Kamu sudah set header CORS tapi lupa return response untuk OPTIONS request. Browser nunggu preflight response yang tidak pernah datang.
- Header di set setelah response body: Di PHP, kalau ada output sebelum
header(), header tidak terkirim. Ini sering terjadi kalau ada BOM character di awal file. - Proxy menimpa header: Nginx atau Apache reverse proxy bisa menimpa header CORS yang sudah diset oleh aplikasi backend.
- CORS error bukan berarti server error: Server bisa saja mengembalikan 200 OK, tapi browser tetap blokir karena header CORS tidak sesuai.
Kesimpulan
CORS error memang bikin kesal, tapi sebenarnya ini adalah fitur keamanan yang penting. Kuncinya ada di tiga hal: pastikan header Access-Control-Allow-Origin diset dengan origin yang tepat, selalu handle preflight OPTIONS request, dan perhatikan urutan header di PHP.
Untuk development, kamu bisa pakai wildcard origin atau proxy di sisi frontend (misalnya Vite punya proxy config). Tapi untuk production, selalu set origin spesifik dan minimal methods yang dibutuhkan. Keamanan nggak boleh dikorbanin demi kemudahan.
Kalau kamu punya pertanyaan atau pengalaman unik soal CORS, tulis di kolom komentar. Siapa tahu ada case yang belum pernah saya temui sebelumnya.