Kalau kamu pernah menulis kode frontend, kemungkinan besar kamu sudah berkenalan dengan error CORS yang bikin pusing itu. Kamu menyiapkan sebuah fetch ke API, klik refresh, buka konsol browser, dan di situlah ia muncul dengan teks merah menyala: ada keterangan soal “blocked by CORS policy”. Permintaannya kelihatan baik-baik saja. API-nya jalan kalau dibuka di tab baru. Lalu kenapa browser menolak menyerahkan respons itu ke kamu?
Jawaban singkatnya: CORS bukan bug, dan browser juga tidak sedang rewel. Ia adalah mekanisme keamanan yang sedang menjalankan tugasnya dengan benar. Tapi untuk benar-benar paham, kita perlu mundur sebentar dan membahas satu aturan yang jadi fondasi seluruh web: same-origin policy. Begitu bagian ini nyangkut di kepala, CORS tidak lagi terasa seperti penghalang acak, melainkan seperti sebuah pintu berkunci yang memang masuk akal.
Apa arti “origin” sebenarnya
Sebelum apa pun, mari kita kunci dulu kata origin, karena seluruh pembahasan ini bergantung padanya. Origin adalah gabungan dari tiga hal dalam sebuah URL:
- skema (seperti
httpatauhttps), - host (seperti
blog.acy-partner.com), - dan port (seperti
443atau3000).
Kalau ketiganya cocok antara dua URL, keduanya berada di origin yang sama. Kalau ada satu saja yang berbeda, keduanya origin yang berbeda. Bagian terakhir inilah yang sering mengecoh pemula, jadi mari lihat tabel konkret berikut:
| URL A | URL B | Origin sama? | Alasan |
|---|---|---|---|
https://acy-partner.com/page |
https://acy-partner.com/other |
Ya | Skema, host, port semua cocok |
https://acy-partner.com |
http://acy-partner.com |
Tidak | Skema beda (https vs http) |
https://acy-partner.com |
https://blog.acy-partner.com |
Tidak | Host beda (subdomain ikut dihitung) |
https://acy-partner.com |
https://acy-partner.com:8080 |
Tidak | Port beda |
Perhatikan, path (/page vs /other) tidak memengaruhi origin. Yang penting hanya skema, host, dan port. Dan benar, subdomain seperti blog. diperlakukan sebagai origin yang sama sekali terpisah dari domain utamanya.
Same-origin policy, dijelaskan sederhana
Same-origin policy adalah aturan yang sudah tertanam di setiap browser. Dalam satu kalimat: sebuah halaman web boleh mengirim permintaan ke origin lain, tapi secara default ia tidak boleh membaca respons dari origin tersebut.
Perbedaan antara mengirim dan membaca inilah inti dari seluruh topik ini, jadi mari kita pelan-pelan di sini. Browser tidak masalah membiarkan satu situs memicu permintaan ke situs lain. Itu hal yang normal dan memang perlu, karena gambar, skrip, font, dan stylesheet terus-menerus dimuat lintas origin. Yang dijaga ketat oleh browser adalah apakah JavaScript di halaman tersebut boleh melihat data respons yang kembali.
Kenapa seketat itu? Bayangkan kamu sedang login ke akun bank di satu tab. Di tab lain, kamu membuka situs yang mencurigakan. Tanpa same-origin policy, JavaScript di situs mencurigakan itu bisa diam-diam membuat permintaan ke API bank kamu. Karena kamu sedang login, browser otomatis akan menyertakan cookie sesi kamu, dan bank akan merespons dengan detail rekening kamu. Kalau halaman mencurigakan tadi bisa membaca respons itu, ia bisa mencuri semuanya. Same-origin policy adalah tembok yang menghalangi halaman tersebut membaca apa yang kembali, walaupun secara teknis permintaannya tetap terkirim.
Mengirim versus membaca
Browser biasanya tetap membiarkan permintaan lintas origin keluar. Yang ia blokir adalah halamanmu membaca responsnya. Itulah kenapa panggilan API-mu bisa sampai ke server dan bahkan menjalankan kode server, tapi JavaScript-mu tetap hanya menerima error.
Lalu di mana CORS masuk?
Same-origin policy adalah default yang masuk akal, tapi terlalu ketat untuk web modern. Setiap saat, aplikasi yang sah perlu berkomunikasi lintas origin. Frontend yang di-hosting di https://acy-partner.com mungkin perlu memanggil API di https://api.acy-partner.com. Aplikasi single-page di satu domain mungkin menarik data dari layanan publik di domain lain. Memblokir semua itu akan membuat banyak arsitektur yang berguna jadi mustahil.
CORS — Cross-Origin Resource Sharing — adalah cara resmi untuk melonggarkan same-origin policy secara terkendali. Ini ide kuncinya, dan layak diingat baik-baik:
CORS adalah server yang memberi izin. Server memberi tahu browser, “Saya tidak keberatan origin lain ini membaca respons saya.” Browser baru melonggarkan aturannya kalau server secara eksplisit menyatakannya.
Ini pergeseran cara berpikir yang penting bagi pemula. CORS bukan sesuatu yang kamu atur di frontend untuk “mengizinkan” permintaanmu. Izinnya ada di server yang memiliki datanya. Browser adalah penegaknya; server adalah pihak yang memberi izin.
Header yang menjalankan tugasnya
Cara server memberi izin ini adalah lewat sebuah header respons HTTP bernama Access-Control-Allow-Origin. Ketika browser menerima respons lintas origin, ia mencari header ini untuk memutuskan apakah halaman boleh membaca isinya.
Pertukaran sederhana yang berhasil terlihat seperti ini:
GET /data HTTP/1.1
Host: api.acy-partner.com
Origin: https://acy-partner.com
HTTP/1.1 200 OK
Access-Control-Allow-Origin: https://acy-partner.com
Content-Type: application/json
{ "message": "Halo dari API" }
Ada dua hal yang terjadi. Browser, karena tahu ini panggilan lintas origin, otomatis menyertakan header Origin yang menyebutkan dari mana permintaan itu berasal. Server merespons dengan Access-Control-Allow-Origin yang menggemakan origin yang sama, dan ini memberi tahu browser, “ya, origin ini boleh membacaku.” Browser lalu menyerahkan JSON itu ke JavaScript kamu.
Kalau header itu tidak ada, atau menyebut origin yang berbeda, browser menolak menampilkan responsnya — dan itulah error CORS yang kamu lihat di konsol. Responsnya sering kali sebenarnya sudah sampai; browser hanya mencabiknya sebelum kodemu sempat menyentuhnya.
Nilai Access-Control-Allow-Origin |
Efek |
|---|---|
https://acy-partner.com |
Hanya origin persis itu yang boleh membaca respons |
* (wildcard) |
Origin mana pun boleh membaca — dipakai untuk data yang benar-benar publik |
| (header tidak ada) | Tidak ada pembacaan lintas origin yang diizinkan; browser memblokir |
Wildcard punya batas
Access-Control-Allow-Origin: * membuat origin mana pun bisa membaca respons, tapi ia tidak bisa digabung dengan permintaan ber-kredensial (yang mengirim cookie). Kalau permintaanmu perlu mengirim cookie, server harus menyebut origin spesifik dan juga mengirim Access-Control-Allow-Credentials: true. Wildcard plus kredensial ditolak browser secara sengaja.
Preflight: ketukan sopan sebelum pintu dibuka
Sampai sini kita membahas permintaan sederhana. Tapi beberapa permintaan dianggap cukup berisiko sampai-sampai browser tidak langsung mengirimnya. Sebagai gantinya, ia mengirim sebuah permintaan kecil “boleh tidak?” lebih dulu. Ini disebut preflight, dan ia memakai metode HTTP OPTIONS.
Sebuah permintaan memicu preflight ketika ia bukan permintaan “sederhana” — misalnya saat memakai metode seperti PUT atau DELETE, atau saat mengirim header seperti Content-Type: application/json atau header Authorization kustom. Logikanya bersifat melindungi: permintaan semacam itu bisa mengubah data di server, jadi browser memeriksa izinnya sebelum melakukan apa pun yang nyata.
Berikut alur lengkap untuk permintaan yang butuh preflight:
Browser (origin-A) Server (origin-B)
| |
| 1. OPTIONS /orders (preflight) |
| Origin: origin-A |
| Access-Control-Request-Method: PUT |
| --------------------------------------> |
| |
| 2. 204 No Content |
| Access-Control-Allow-Origin: A |
| Access-Control-Allow-Methods: PUT |
| <-------------------------------------- |
| |
| 3. PUT /orders yang sebenarnya |
| --------------------------------------> |
| |
| 4. 200 OK + Access-Control-Allow-Origin|
| <-------------------------------------- |
Di langkah 1 browser bertanya, tanpa mengirim payload aslinya, “Saya mau membuat permintaan PUT dari origin-A dengan header-header ini — apakah diizinkan?” Di langkah 2 server menjawab origin, metode, dan header apa saja yang diizinkannya. Hanya kalau jawabannya mencakup apa yang dibutuhkan permintaan asli, barulah browser lanjut ke langkah 3 dan mengirim PUT yang sebenarnya. Kalau preflight ditolak, permintaan aslinya bahkan tidak pernah keluar dari browser.
Header-header yang terlibat dalam respons preflight layak dijadikan rujukan singkat:
| Header respons | Arti |
|---|---|
Access-Control-Allow-Origin |
Origin mana yang diizinkan |
Access-Control-Allow-Methods |
Metode HTTP apa yang diizinkan (mis. GET, POST, PUT) |
Access-Control-Allow-Headers |
Header permintaan apa yang diizinkan (mis. Content-Type, Authorization) |
Access-Control-Max-Age |
Berapa lama browser boleh menyimpan hasil preflight ini, dalam detik |
Yang terakhir, Access-Control-Max-Age, adalah optimasi yang manis: ia membuat browser bisa mengingat jawaban preflight untuk sementara, sehingga tidak perlu bertanya sebelum setiap permintaan.
Kenapa pemula terus-menerus kena error CORS
Sekarang setelah semua kepingnya tersusun, pengalaman klasik pemula jadi masuk akal. Kamu membangun frontend di http://localhost:3000 dan API di http://localhost:5000. Port yang berbeda berarti origin yang berbeda, jadi browser menegakkan CORS. API kamu tidak pernah mengeset Access-Control-Allow-Origin, jadi browser memblokir pembacaannya, dan kamu pun kena error — padahal server-nya sendiri sudah merespons dengan sempurna.
Beberapa hal yang membingungkan orang di tahap ini:
- Error-nya adalah ulah browser, bukan server. Alat seperti
curlatau Postman tidak menegakkan same-origin policy, makanya API-mu “jalan” di sana tapi gagal di browser. Alat-alat itu bukan browser, jadi tidak ada halaman yang perlu dilindungi. - Kamu tidak bisa memperbaiki CORS murni dari frontend. Izinnya harus datang dari header respons server. Sebanyak apa pun kamu mengutak-atik opsi fetch, browser tidak akan memercayai origin yang tidak pernah disetujui server.
- Preflight yang gagal terlihat seperti permintaan yang gagal. Kalau panggilan
OPTIONS-mu ditolak, permintaan asli tidak pernah berjalan, dan konsol mungkin menunjuk ke panggilan utama, menyembunyikan fakta bahwa preflight-lah biang keladinya.
Solusinya ada di server
Saat kena error CORS, tanyakan: “Halaman saya ada di origin apa, dan apakah respons server menyertakan Access-Control-Allow-Origin yang cocok dengannya?” Solusinya hampir selalu mengonfigurasi server (atau middleware CORS-nya) agar mengizinkan origin frontend kamu — bukan mengubah browser atau frontend.
Rangkuman singkat
Mari kita rangkai semuanya. Same-origin policy adalah aturan keamanan browser: sebuah halaman boleh mengirim permintaan ke origin lain tapi, secara default, tidak boleh membaca responsnya. Origin adalah trio skema, host, dan port — ubah salah satu saja, kamu sudah punya origin baru. Aturan ini ada untuk menghalangi halaman berbahaya diam-diam membaca data pribadimu di situs tempat kamu sedang login.
CORS adalah cara terkendali untuk melonggarkan aturan itu. Cara kerjanya lewat server yang memberi izin: ia mengirim header Access-Control-Allow-Origin (beserta kawan-kawannya) yang memberi tahu browser origin mana yang boleh membaca responsnya. Untuk permintaan yang lebih berisiko, browser lebih dulu mengirim permintaan preflight OPTIONS untuk memastikan izin sebelum mengirim yang sebenarnya. Error CORS yang terkenal itu sebenarnya hanya berarti browser tidak menemukan izin yang bisa ia percaya, jadi ia menolak menampilkan respons ke kodemu.
Begitu kamu benar-benar paham bahwa CORS adalah izin yang diberikan server dan ditegakkan browser, semuanya berubah dari tembok yang menjengkelkan menjadi sebuah kontrak yang jelas dan masuk akal. Dari sini, langkah lanjutan yang wajar adalah menengok lebih dekat bagaimana cookie dan header Access-Control-Allow-Credentials bekerja bersama, karena permintaan lintas origin ber-kredensial punya aturannya sendiri yang harus cermat — dan di situlah banyak teka-teki CORS dunia nyata yang lebih rumit bersembunyi.