CORS #
Cross-Origin Resource Sharing (CORS) adalah protokol keamanan berbasis standar web yang mendefinisikan cara bagi aplikasi web yang berjalan di satu domain (origin) untuk mengakses sumber daya di domain lain secara aman. Di era arsitektur web modern di mana repositori frontend (seperti React, Svelte, Vue, atau Angular) dideploy pada domain atau subdomain terpisah dari API backend, pemahaman dan konfigurasi CORS yang tepat menjadi hal yang sangat krusial. Kegagalan dalam mengonfigurasi CORS tidak hanya akan memblokir komunikasi antara frontend dan backend, tetapi kesalahan konfigurasi yang terlalu longgar juga dapat membuka celah keamanan yang membahayakan data pengguna kita. Kita akan mengupas tuntas prinsip dasar Same-Origin Policy (SOP), anatomi permintaan pra-terbang (preflight request), cara mengimplementasikan header CORS di Caddy secara efisien, teknik penanganan kredensial, pengelolaan daftar izin (whitelist) domain dinamis, perbandingan CORS di tingkat proxy versus tingkat backend, serta strategi praktis untuk mendiagnosis dan memecahkan pesan kesalahan CORS yang sering membingungkan developer.
Ketersediaan dan Pendekatan CORS di Caddy #
Caddy dirancang dengan filosofi minimalis dan modular. Oleh karena itu, Caddy tidak memiliki direktif bawaan bernama cors di dalam konfigurasi standarnya. Keputusan desain ini diambil karena fungsi CORS pada dasarnya dapat diimplementasikan sepenuhnya menggunakan manipulasi header HTTP dan pencocokan permintaan (request matching).
Di Caddy, kita menangani CORS menggunakan dua fitur bawaan utama:
- Direktif
header: Digunakan untuk menambahkan, memodifikasi, atau menghapus header respons HTTP yang diperlukan oleh spesifikasi CORS. - Matcher (Pencocok): Digunakan untuk mendeteksi metode permintaan tertentu (seperti
OPTIONSuntuk request preflight) atau untuk menyaring nilai headerOriginyang dikirimkan oleh browser klien.
Pendekatan ini memberikan kita kendali penuh atas bagaimana server merespons permintaan lintas domain tanpa bergantung pada modul tambahan. Kita tidak perlu menginstal plugin pihak ketiga menggunakan xcaddy hanya untuk mengaktifkan CORS dasar hingga tingkat lanjut. Semua logika penanganan CORS dapat ditulis langsung menggunakan Caddyfile standar yang sangat bersih dan mudah dipelihara.
Memahami Konsep Same-Origin Policy dan Mengapa CORS Diperlukan #
Sebelum kita mengonfigurasi CORS di Caddy, kita harus memahami mengapa mekanisme ini ada di browser. Pondasi utama keamanan web adalah Same-Origin Policy (SOP). SOP adalah aturan keamanan ketat yang diterapkan oleh semua browser modern untuk mencegah skrip (seperti JavaScript yang dijalankan melalui fetch atau XMLHttpRequest) di satu halaman web membaca data sensitif dari halaman web lain di origin yang berbeda.
Apa itu Origin? #
Dalam konteks keamanan web, sebuah Origin didefinisikan sebagai kombinasi dari tiga komponen:
- Skema (Protocol): Misalnya
httpatauhttps. - Inang (Host/Domain): Misalnya
example.comatauapi.example.com. - Port: Misalnya
80,443, atau8080.
Dua URL dikatakan memiliki Same Origin (asal yang sama) hanya jika ketiga komponen tersebut identik. Jika salah satu komponen berbeda, browser akan menganggapnya sebagai Cross Origin (lintas asal).
Mari kita lihat tabel ilustrasi perbandingan origin berikut dengan target referensi utama: https://api.example.com:443/v1/users
| URL Klien yang Meminta | Komponen yang Berbeda | Kategori Hubungan | status Akses SOP Bawaan |
|---|---|---|---|
https://api.example.com/v1/profiles | Tidak Ada (Port default HTTPS adalah 443) | Same Origin | Diizinkan (Akses Penuh) |
http://api.example.com/v1/users | Skema (http vs https) | Cross Origin | Diblokir oleh SOP |
https://api.example.com:8443/v1/users | Port (8443 vs 443) | Cross Origin | Diblokir oleh SOP |
https://www.example.com/v1/users | Host (www.example.com vs api.example.com) | Cross Origin | Diblokir oleh SOP |
https://example.com/v1/users | Host (example.com vs api.example.com) | Cross Origin | Diblokir oleh SOP |
Mengapa SOP Sangat Ketat? #
Bayangkan jika SOP tidak ada. Jika kita sedang masuk ke akun perbankan kita di https://bank.example.com, browser akan menyimpan cookie sesi kita. Kemudian, kita tidak sengaja membuka situs berbahaya https://jahat.example.com di tab lain. Tanpa SOP, JavaScript yang berjalan di https://jahat.example.com dapat dengan mudah mengirimkan perintah fetch ke https://bank.example.com/transfer untuk mengirim uang kita ke rekening penyerang. Browser secara otomatis akan menyertakan cookie sesi bank kita karena request ditujukan ke domain bank.
SOP mencegah skrip dari https://jahat.example.com untuk membaca respons dari bank kita. Namun, dalam skenario aplikasi modern, kita sering kali secara sengaja memisahkan domain frontend (https://dashboard.example.com) dan domain backend API (https://api.example.com). Di sinilah CORS masuk sebagai mekanisme resmi yang aman bagi server backend untuk memberitahu browser: “Saya mempercayai domain frontend ini, silakan izinkan JavaScript dari domain tersebut untuk membaca respons saya.”
Mekanisme Jabat Tangan CORS (CORS Handshake) #
Ketika aplikasi frontend mencoba melakukan request lintas domain, browser akan membagi permintaan tersebut menjadi dua kategori berdasarkan tingkat risikonya: Simple Request (permintaan sederhana) dan Non-Simple Request (permintaan tidak sederhana/memerlukan preflight).
1. Permintaan Sederhana (Simple Request) #
Browser mengategorikan permintaan sebagai Simple Request jika memenuhi kriteria berikut secara kumulatif:
- Menggunakan metode HTTP:
GET,POST, atauHEAD. - Hanya menggunakan header bawaan aman seperti
Accept,Accept-Language,Content-Language, danContent-Type. - Nilai header
Content-Typeyang diperbolehkan hanya terbatas pada:application/x-www-form-urlencoded,multipart/form-data, atautext/plain.
Untuk Simple Request, browser akan langsung mengirimkan permintaan asli ke server. Namun, browser menyertakan header tambahan bernama Origin (misalnya Origin: https://dashboard.example.com). Server akan memproses request tersebut, dan ketika mengembalikan respons, server harus menyertakan header Access-Control-Allow-Origin yang berisi domain klien tersebut. Jika browser melihat header respons tersebut tidak cocok dengan origin asal, browser akan menyembunyikan respons tersebut dari JavaScript frontend kita dan memicu error.
2. Permintaan dengan Preflight (Non-Simple Request) #
Jika request kita menggunakan metode kustom (seperti PUT, DELETE, PATCH), mengirimkan tipe data seperti application/json, atau menyertakan header kustom (misalnya header Authorization untuk autentikasi token JWT), browser menganggap request tersebut memiliki risiko keamanan lebih tinggi.
Untuk mencegah request berbahaya merusak data di server sebelum diotorisasi, browser akan melakukan mekanisme OPTIONS Preflight Request (permintaan pra-terbang). Browser akan mengirimkan permintaan awal dengan metode OPTIONS ke server terlebih dahulu untuk memverifikasi apakah server mengizinkan request asli tersebut.
Mari kita lihat alur komunikasi preflight melalui diagram urutan berikut:
sequenceDiagram
participant Browser as "Browser Klien"
participant Caddy as "Caddy Web Server"
participant Backend as "Aplikasi Backend"
Note over Browser: 1. Mendeteksi request non-sederhana (e.g. Content-Type: application/json)
Browser->>Caddy: OPTIONS /api/resource HTTP/1.1<br/>Origin: https://dashboard.example.com<br/>Access-Control-Request-Method: PUT<br/>Access-Control-Request-Headers: Authorization, Content-Type
Note over Caddy: 2. Caddy memproses OPTIONS secara langsung<br/>Mengecek whitelist domain & menyusun header CORS
Caddy-->>Browser: HTTP/1.1 204 No Content<br/>Access-Control-Allow-Origin: https://dashboard.example.com<br/>Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS<br/>Access-Control-Allow-Headers: Authorization, Content-Type<br/>Access-Control-Max-Age: 86400<br/>Access-Control-Allow-Credentials: true
Note over Browser: 3. Verifikasi preflight sukses di browser<br/>Mengirimkan request asli
Browser->>Caddy: PUT /api/resource HTTP/1.1<br/>Origin: https://dashboard.example.com<br/>Authorization: Bearer token_jwt_kita<br/>Content-Type: application/json
Caddy->>Backend: Meneruskan Request PUT asli
Backend-->>Caddy: Respons Data dari Aplikasi (HTTP 200 OK)
Caddy-->>Browser: HTTP/1.1 200 OK<br/>Access-Control-Allow-Origin: https://dashboard.example.com<br/>Access-Control-Allow-Credentials: trueDalam jabat tangan di atas:
Origin: Menyatakan asal aplikasi frontend yang meminta akses.Access-Control-Request-Method: Memberitahu server metode apa yang akan digunakan pada request asli.Access-Control-Request-Headers: Memberitahu server header kustom apa saja yang akan dikirim pada request asli.Access-Control-Max-Age: Menentukan berapa lama browser diperbolehkan menyimpan cache hasil verifikasi preflight ini (dalam hitungan detik). Selama masa cache ini belum kedaluwarsa, browser tidak akan mengirim requestOPTIONSlagi untuk endpoint yang sama, sehingga menghemat bandwidth dan meningkatkan performa aplikasi kita.
Konfigurasi CORS Dasar di Caddy #
Sekarang kita akan menulis konfigurasi Caddyfile dasar untuk menangani CORS. Kita berasumsi bahwa aplikasi frontend kita berjalan di https://dashboard.example.com dan ingin mengakses API di https://api.example.com.
Penulisan Caddyfile Dasar #
# Contoh: Konfigurasi CORS Dasar untuk Origin Tunggal
api.example.com {
# 1. Definisikan matcher untuk mendeteksi request preflight OPTIONS
@options method OPTIONS
# 2. Tangani request OPTIONS secara langsung di Caddy
handle @options {
header Access-Control-Allow-Origin "https://dashboard.example.com"
header Access-Control-Allow-Methods "GET, POST, PUT, DELETE, PATCH, OPTIONS"
header Access-Control-Allow-Headers "Content-Type, Authorization, X-Requested-With"
header Access-Control-Max-Age "86400"
# Kembalikan status 204 (No Content) langsung tanpa meneruskan ke backend
respond "" 204
}
# 3. Tambahkan header CORS untuk request biasa (GET, POST, dll.)
header Access-Control-Allow-Origin "https://dashboard.example.com"
header Access-Control-Allow-Methods "GET, POST, PUT, DELETE, PATCH, OPTIONS"
# 4. Teruskan lalu lintas ke backend aplikasi kita
reverse_proxy localhost:8080
}
Penjelasan Baris Konfigurasi #
@options method OPTIONS: Kita membuat matcher bernama@optionsyang hanya akan mencocokkan request dengan metode HTTPOPTIONS. Ini memastikan rute preflight ditangkap dengan tepat.handle @options { ... }: Blok penanganan khusus untuk memotong request preflight. Sangat penting bagi kita untuk langsung mengembalikan respons dari Caddy (respond "" 204) agar aplikasi backend kita di port8080tidak dibebani oleh permintaanOPTIONS. Hal ini meningkatkan efisiensi komputasi server backend kita secara signifikan.Access-Control-Max-Age "86400": Kita menginstruksikan browser untuk menyimpan informasi preflight ini dalam cache selama 24 jam (86.400 detik). Ini sangat membantu performa aplikasi frontend karena menghilangkan delay ekstra akibat request preflight berulang.
CORS dengan Kredensial (Cookies & Auth Headers) #
Ketika aplikasi frontend kita perlu mengirimkan informasi kredensial—seperti Cookie Sesi, header HTTP Authorization (misalnya token JWT kustom), atau sertifikat TLS klien—kita harus mengaktifkan dukungan kredensial secara eksplisit pada konfigurasi CORS kita.
Aturan Keamanan Browser untuk Kredensial #
Jika request lintas domain menyertakan kredensial (misalnya pada fetch API disetel opsi { credentials: 'include' }), browser menerapkan aturan keamanan yang sangat ketat:
- Wajib menyertakan
Access-Control-Allow-Credentials: truepada respons dari server. Jika header ini tidak ada atau bernilaifalse, browser akan menolak memberikan respons data ke frontend kita. - JANGAN sekali-kali menggunakan wildcard
*pada headerAccess-Control-Allow-Origin. Jika server mengembalikanAccess-Control-Allow-Origin: *sementara request menyertakan kredensial, browser akan memblokir request tersebut demi keamanan. Server wajib mengembalikan domain origin yang spesifik dan valid.
Penulisan Caddyfile untuk Kredensial #
# Contoh: Konfigurasi CORS dengan Kredensial Ketat
api.example.com {
@options method OPTIONS
handle @options {
# ✓ BENAR: Tentukan origin secara spesifik, bukan menggunakan wildcard (*)
header Access-Control-Allow-Origin "https://dashboard.example.com"
header Access-Control-Allow-Credentials "true"
header Access-Control-Allow-Methods "GET, POST, PUT, DELETE, PATCH, OPTIONS"
header Access-Control-Allow-Headers "Content-Type, Authorization, Cookie, X-CSRF-Token"
header Access-Control-Max-Age "86400"
respond "" 204
}
# Header CORS untuk request biasa
header Access-Control-Allow-Origin "https://dashboard.example.com"
header Access-Control-Allow-Credentials "true"
# Mengekspos header kustom agar bisa dibaca oleh JavaScript frontend kita
header Access-Control-Expose-Headers "X-Total-Count, X-Request-ID"
reverse_proxy localhost:8080
}
Menggunakan Access-Control-Expose-Headers
#
Secara default, browser membatasi akses JavaScript frontend kita untuk membaca header respons. Frontend hanya diizinkan membaca beberapa header dasar seperti Cache-Control, Content-Language, Content-Type, Expires, Last-Modified, dan Pragma.
Jika backend aplikasi kita mengirimkan informasi penting lewat header kustom—misalnya jumlah total data untuk paginasi di X-Total-Count, atau ID unik pelacakan di X-Request-ID—kita wajib menambahkannya di header Access-Control-Expose-Headers Caddyfile agar JavaScript frontend kita dapat mengaksesnya melalui method response.headers.get().
Mengelola Banyak Origin Secara Dinamis (Dynamic Whitelist) #
Salah satu keterbatasan terbesar dari spesifikasi header HTTP CORS adalah nilai Access-Control-Allow-Origin hanya dapat berupa wildcard * atau satu URL domain tunggal. Kita tidak diperbolehkan memasukkan daftar domain yang dipisahkan koma seperti ini:
// ANTI-PATTERN: Format comma-separated ini tidak valid dan akan ditolak oleh browser
Access-Control-Allow-Origin: https://dashboard.example.com, https://admin.example.com
Lalu, bagaimana jika kita memiliki beberapa lingkungan frontend yang sah? Misalnya lingkungan lokal untuk development (http://localhost:3000), lingkungan staging (https://staging.example.com), dan lingkungan produksi (https://dashboard.example.com).
Solusi Dinamis di Caddy #
Solusi untuk skenario ini adalah dengan memeriksa header Origin yang dikirim oleh request browser klien. Jika origin tersebut terdapat di dalam daftar izin (whitelist) kita, kita akan mengambil string origin tersebut dan mengembalikannya secara dinamis ke header respons Access-Control-Allow-Origin.
Kita dapat menggunakan variabel internal Caddy {http.request.header.Origin} (atau cukup disingkat {header.Origin}) untuk melakukan hal ini.
Mari kita lihat alur logika evaluasi di Caddy pada diagram berikut:
flowchart TD
Start["1. Request Masuk di Caddy"] --> GetOrigin["2. Baca Header 'Origin' dari Klien"]
GetOrigin --> CheckMatch{"3. Apakah Origin ada di Whitelist?\n(e.g., localhost:3000, dashboard.example.com)"}
CheckMatch -- "Ya" --> IsOptions{"4. Apakah Metode OPTIONS (Preflight)?"}
CheckMatch -- "Tidak" --> RejectCORS["5. Teruskan Request tanpa Header CORS\n(Browser akan memblokir respons)"]
IsOptions -- "Ya" --> SetFullCORS["6. Setel respons dengan:\nAccess-Control-Allow-Origin = {header.Origin}\nAccess-Control-Allow-Credentials = true\nAccess-Control-Max-Age = 86400"]
SetFullCORS --> Return204["7. Kembalikan respons status 204\n(Langsung dari Caddy)"]
IsOptions -- "Tidak" --> SetSimpleCORS["8. Setel respons dengan:\nAccess-Control-Allow-Origin = {header.Origin}\nAccess-Control-Allow-Credentials = true"]
SetSimpleCORS --> ForwardBackend["9. Teruskan request asli ke Backend\n(reverse_proxy)"]Penulisan Caddyfile Dynamic Whitelist #
Berikut adalah konfigurasi Caddyfile untuk mengimplementasikan alur logika di atas:
# Contoh: CORS dengan Whitelist Domain Dinamis & Kredensial
api.example.com {
# 1. Definisikan matcher untuk mendeteksi origin yang diizinkan
@allowed_origins {
header Origin "http://localhost:3000"
header Origin "https://staging.example.com"
header Origin "https://dashboard.example.com"
header Origin "https://admin.example.com"
}
# 2. Definisikan matcher untuk mendeteksi request preflight OPTIONS
@options method OPTIONS
# 3. Tangani preflight request untuk origin yang diizinkan
handle @options {
# Gunakan matcher bawaan untuk memvalidasi request origin
@options_allowed {
expression {header.Origin} != ""
header Origin "http://localhost:3000"
header Origin "https://staging.example.com"
header Origin "https://dashboard.example.com"
header Origin "https://admin.example.com"
}
handle @options_allowed {
# Kembalikan origin request klien secara dinamis
header Access-Control-Allow-Origin "{header.Origin}"
header Access-Control-Allow-Credentials "true"
# Beritahu cache proxy/browser untuk menyimpan respons berdasarkan Origin
header Vary "Origin"
header Access-Control-Allow-Methods "GET, POST, PUT, DELETE, PATCH, OPTIONS"
header Access-Control-Allow-Headers "Content-Type, Authorization, Cookie"
header Access-Control-Max-Age "86400"
respond "" 204
}
# Jika request preflight OPTIONS dari origin yang TIDAK diizinkan
# Tolak dengan status 400 Bad Request
respond "Origin Not Allowed" 400
}
# 4. Tangani request biasa dari origin yang diizinkan
handle @allowed_origins {
header Access-Control-Allow-Origin "{header.Origin}"
header Access-Control-Allow-Credentials "true"
header Vary "Origin"
reverse_proxy localhost:8080
}
# 5. Tangani request tanpa origin (misal dari aplikasi mobile native atau curl)
# atau request dari origin yang tidak terdaftar di whitelist
handle {
# Langsung teruskan ke backend tanpa menambahkan header CORS
reverse_proxy localhost:8080
}
}
Pentingnya Header Vary: Origin
#
[!WARNING] Selalu sertakan header
Vary: Originketika menggunakan domain dinamis. Jika kita tidak menyertakan headerVary: Origin, server caching di tengah jalan (seperti CDN, CDN Cloudflare, atau proxy browser) mungkin akan meng-cache respons dengan headerAccess-Control-Allow-Origin: https://dashboard.example.comuntuk pengunjung pertama. Ketika pengunjung berikutnya datang menggunakan originhttps://admin.example.com, CDN akan menyajikan halaman yang di-cache tersebut. Browser pengunjung kedua akan menolak respons karena mendeteksi ketidakcocokan origin. HeaderVary: Originmemberitahu server caching untuk meng-cache respons secara terpisah untuk setiap nilai headerOriginrequest yang berbeda.
CORS untuk API Publik vs API Privat #
Bergantung pada jenis layanan yang kita tawarkan, kita harus membedakan cara penanganan CORS untuk API publik yang ditujukan untuk integrasi pihak ketiga, dan API privat yang digunakan internal.
Perbandingan Karakteristik CORS #
| Kriteria Analisis | API Publik (e.g. Layanan Cuaca, Font) | API Privat (e.g. Dashboard Admin, User Service) |
|---|---|---|
| Kredensial (Cookies/JWT) | Dinonaktifkan (credentials: 'omit') | Diaktifkan (credentials: 'include') |
| Nilai Allow-Origin | Wildcard * | Origin spesifik (statis atau dinamis) |
| Tingkat Risiko Keamanan | Sangat Rendah (Data bersifat terbuka) | Sangat Tinggi (Akses data sensitif) |
| Kebutuhan Header Vary | Tidak Butuh (Sama untuk semua origin) | Sangat Wajib (Vary: Origin untuk caching) |
| Metode HTTP yang Diizinkan | Umumnya hanya GET dan OPTIONS | Semua metode (GET, POST, PUT, DELETE, dll.) |
Implementasi API Publik di Caddy #
Untuk API publik yang tidak menyimpan data sensitif sesi pengguna, kita dapat menggunakan konfigurasi wildcard sederhana yang sangat ramah terhadap caching CDN:
# Contoh: Konfigurasi API Publik Bebas Akses
public-api.example.com {
@options method OPTIONS
handle @options {
header Access-Control-Allow-Origin "*"
header Access-Control-Allow-Methods "GET, POST, OPTIONS"
header Access-Control-Allow-Headers "Content-Type, Authorization"
header Access-Control-Max-Age "86400"
respond "" 204
}
# Izinkan semua domain untuk mengakses endpoint GET publik kita
header Access-Control-Allow-Origin "*"
header Access-Control-Allow-Methods "GET, POST, OPTIONS"
reverse_proxy localhost:8080
}
Mengapa CORS di Tingkat Proxy (Caddy) Lebih Baik daripada di Aplikasi Backend? #
Banyak developer pemula mengonfigurasi CORS di dalam kode aplikasi mereka (misalnya menggunakan middleware Express.js cors(), anotasi Java Spring @CrossOrigin, atau pustaka CORS Django). Meskipun cara ini bekerja, memindahkan logika CORS ke tingkat reverse proxy seperti Caddy memberikan beberapa keuntungan performa dan arsitektur yang signifikan:
- Efisiensi Sumber Daya Backend: Penanganan OPTIONS preflight request dipotong langsung di Caddy (
respond "" 204). Aplikasi backend kita (Node.js, Python, Ruby, Java) tidak perlu menghabiskan thread CPU, alokasi memori, atau koneksi database hanya untuk memproses request preflight yang tidak membawa payload data asli. - Kecepatan Respons Preflight: Caddy ditulis dalam bahasa pemrograman Go yang sangat cepat. Respons preflight dari Caddy dapat dikembalikan ke browser klien dalam hitungan milidetik, jauh lebih cepat dibanding jika permintaan tersebut harus masuk ke aplikasi backend kita melewati tumpukan routing framework.
- Sentralisasi Kebijakan Keamanan: Kita dapat mengelola kebijakan CORS di satu tempat (Caddyfile) untuk puluhan mikroservis backend yang ditulis dalam bahasa pemrograman berbeda. Hal ini mencegah terjadinya inkonsistensi kebijakan keamanan di tingkat internal server.
Troubleshooting dan Penanganan Error CORS Umum #
Masalah CORS adalah salah satu masalah paling sering ditemui dalam pengembangan aplikasi web modern. Berikut adalah daftar kesalahan umum yang sering dilaporkan di konsol developer browser beserta solusi konkrit untuk mengatasinya.
1. Error: “No ‘Access-Control-Allow-Origin’ header is present” #
Pesan Kesalahan di Konsol Browser:
Access to fetch at 'https://api.example.com/data' from origin 'https://dashboard.example.com' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.
Kemungkinan Penyebab & Solusi:
- Penyebab A: Konfigurasi CORS belum ditambahkan di Caddyfile untuk domain
api.example.com.- Solusi: Tambahkan direktif
header Access-Control-Allow-Originsesuai panduan di atas.
- Solusi: Tambahkan direktif
- Penyebab B: Domain origin yang dikirim oleh browser (
https://dashboard.example.com) tidak terdaftar atau salah eja pada daftar whitelist di Caddyfile.- Solusi: Periksa ejaan domain, skema (
httpvshttps), dan port secara teliti.
- Solusi: Periksa ejaan domain, skema (
- Penyebab C: Server backend API mengalami error internal (
HTTP 500 Internal Server Error) atau data tidak ditemukan (HTTP 404 Not Found). Secara default di Caddy, jika backend mengembalikan error, direktifheaderstandar tidak akan dieksekusi sehingga header CORS hilang dari respons.- Solusi: Gunakan kata kunci
deferpada direktifheaderdi Caddyfile. Opsidefermenginstruksikan Caddy untuk menambahkan header tepat sebelum respons ditulis ke jaringan, apa pun status kode HTTP yang dihasilkan oleh backend.
- Solusi: Gunakan kata kunci
# Contoh Solusi: Menggunakan opsi 'defer' untuk menjamin header CORS tetap ada saat Backend error
header Access-Control-Allow-Origin "https://dashboard.example.com" {
defer
}
2. Error: “Access-Control-Allow-Origin must not be wildcard ‘*’ when credentials mode is ‘include’” #
Pesan Kesalahan di Konsol Browser:
Access to fetch at 'https://api.example.com/data' from origin 'https://dashboard.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'.
Penyebab & Solusi:
Aplikasi frontend mengirimkan permintaan dengan mengaktifkan opsi kredensial (seperti menyertakan cookie), namun di sisi Caddy, kita menyetel header Access-Control-Allow-Origin ke wildcard *.
- Solusi: Ubah konfigurasi Caddyfile agar mengembalikan domain spesifik (statis atau menggunakan pendekatan whitelist dinamis
{header.Origin}).
3. Error: “Response to preflight request doesn’t pass access control check: It does not have HTTP ok status” #
Pesan Kesalahan di Konsol Browser:
Access to fetch at 'https://api.example.com/data' from origin 'https://dashboard.example.com' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: It does not have HTTP ok status.
Penyebab & Solusi:
Request preflight OPTIONS yang dikirim oleh browser diblokir atau gagal diproses oleh Caddy atau backend. Misalnya, kita melindungi endpoint API dengan Basic Auth, sehingga request OPTIONS ikut ditolak dengan status 401 Unauthorized karena tidak membawa kredensial autentikasi.
- Solusi: Kita wajib memastikan bahwa request dengan metode
OPTIONSdibebaskan dari pengecekan autentikasi, pembatasan IP, atau rate limiting. Browser tidak pernah menyertakan kredensial autentikasi pada OPTIONS preflight request.
# Contoh Solusi: Membebaskan rute OPTIONS dari Basic Auth
api.example.com {
# Matcher untuk request non-OPTIONS
@needs_auth {
not method OPTIONS
path /admin/*
}
# Basic auth hanya dijalankan jika method BUKAN OPTIONS
basicauth @needs_auth {
admin $2a$14$8lGvWLMR9jGg2.bSZlAHOeYuI1FjTPExECWQpkLPMH1y0LkJnbEKy
}
# OPTIONS preflight request ditangani langsung di atas
@options method OPTIONS
handle @options {
header Access-Control-Allow-Origin "https://dashboard.example.com"
header Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS"
header Access-Control-Allow-Headers "Content-Type, Authorization"
header Access-Control-Max-Age "86400"
respond "" 204
}
header Access-Control-Allow-Origin "https://dashboard.example.com"
reverse_proxy localhost:8080
}
Simulasi Pengujian CORS Menggunakan curl
#
Kita tidak perlu selalu membuka browser dan memicu script JavaScript untuk menguji apakah konfigurasi CORS kita di Caddy sudah benar. Kita dapat melakukan pengujian secara cepat dan akurat menggunakan perintah terminal curl.
1. Menguji OPTIONS Preflight Request #
Untuk menyimulasikan request preflight OPTIONS yang dikirim oleh browser, kita harus mengirimkan header request Origin, Access-Control-Request-Method, dan Access-Control-Request-Headers secara manual:
# Jalankan perintah curl berikut di terminal kita
curl -v -X OPTIONS https://api.example.com/data \
-H "Origin: https://dashboard.example.com" \
-H "Access-Control-Request-Method: PUT" \
-H "Access-Control-Request-Headers: Authorization"
Output Respons yang Diharapkan (Jika Berhasil):
HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://dashboard.example.com
Access-Control-Allow-Methods: GET, POST, PUT, DELETE, PATCH, OPTIONS
Access-Control-Allow-Headers: Content-Type, Authorization
Access-Control-Max-Age: 86400
Access-Control-Allow-Credentials: true
Content-Length: 0
2. Menguji Request Biasa (Actual Request) #
Untuk menguji apakah respons biasa dari server menyertakan header CORS yang sesuai:
# Menguji request GET biasa dengan menyertakan header Origin
curl -i https://api.example.com/data \
-H "Origin: https://dashboard.example.com"
Output Respons yang Diharapkan:
HTTP/1.1 200 OK
Content-Type: application/json
Access-Control-Allow-Origin: https://dashboard.example.com
Access-Control-Allow-Credentials: true
Vary: Origin
{
"status": "success",
"data": []
}
Snippet Konfigurasi CORS Reusable #
Jika kita mengelola banyak domain API di dalam satu berkas konfigurasi Caddyfile, menulis ulang blok @options dan konfigurasi header CORS di setiap blok server akan membuat berkas Caddyfile kita menjadi sangat panjang dan sulit dipelihara.
Untuk menjaga prinsip DRY (Don’t Repeat Yourself), kita dapat membungkus seluruh konfigurasi CORS tersebut ke dalam sebuah Snippet Caddyfile kustom yang dapat di-import kembali dengan mudah.
Penulisan Snippet CORS #
# 1. Definisikan Snippet Reusable untuk CORS Internal kita
(cors_internal) {
# Tentukan domain asal secara dinamis menggunakan argumen snippet {args.0}
@options method OPTIONS
handle @options {
header Access-Control-Allow-Origin "{args.0}"
header Access-Control-Allow-Credentials "true"
header Access-Control-Allow-Methods "GET, POST, PUT, DELETE, PATCH, OPTIONS"
header Access-Control-Allow-Headers "Content-Type, Authorization, X-Requested-With, X-CSRF-Token"
header Access-Control-Max-Age "86400"
respond "" 204
}
header Access-Control-Allow-Origin "{args.0}"
header Access-Control-Allow-Credentials "true"
header Vary "Origin"
}
# 2. Implementasikan snippet di Blok Server API v1
api-v1.example.com {
# Import snippet dan teruskan domain frontend yang diizinkan sebagai argumen pertama
import cors_internal "https://dashboard.example.com"
reverse_proxy localhost:8081
}
# 3. Implementasikan snippet di Blok Server API v2
api-v2.example.com {
# Kita dapat menentukan domain yang berbeda jika diperlukan
import cors_internal "https://admin.example.com"
reverse_proxy localhost:8082
}
Dengan menggunakan snippet, jika suatu saat kita perlu menambahkan header kustom baru ke dalam daftar Access-Control-Allow-Headers kita (misalnya header integrasi monitoring kustom), kita hanya perlu mengeditnya satu kali di dalam blok snippet (cors_internal). Perubahan tersebut akan secara otomatis diterapkan ke seluruh blok server API yang mengimpor snippet tersebut saat kita memicu reload konfigurasi Caddy.
Ringkasan #
- CORS Bukanlah Fitur Tambahan Caddy: Caddy menangani CORS sepenuhnya menggunakan direktif bawaan
headerdan matcher — tidak ada modul eksternal yang perlu dipasang.- OPTIONS Preflight Request: Harus selalu dipotong secara langsung di tingkat Caddy menggunakan matcher
@options method OPTIONSdan respons kosong204 No Contentagar menghemat beban pemrosesan aplikasi backend kita.- Keamanan Kredensial: Saat request frontend menyertakan cookies atau token otentikasi kustom,
Access-Control-Allow-Credentialswajib disetel ketrue, dan nilaiAccess-Control-Allow-Origintidak boleh menggunakan wildcard*.- Daftar Izin Dinamis (Dynamic Whitelist): Kita dapat mencocokkan header request
Originklien menggunakan matcher Caddy, kemudian mengembalikan nilainya secara dinamis melalui placeholder{header.Origin}.- Vary Header: Selalu tambahkan header respons
Vary: Originsaat mengembalikan origin dinamis untuk menghindari terjadinya kesalahan cache respons di tingkat CDN atau proxy browser.- Penyebab CORS Error Tersembunyi: Jika aplikasi backend mengalami kegagalan internal (HTTP status 5xx atau 4xx), pastikan kita menggunakan opsi
deferpada direktifheaderdi Caddy agar header respons CORS tidak terhapus.
← Sebelumnya: Security Headers Berikutnya: Middleware Rewrite →