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:

  1. Direktif header: Digunakan untuk menambahkan, memodifikasi, atau menghapus header respons HTTP yang diperlukan oleh spesifikasi CORS.
  2. Matcher (Pencocok): Digunakan untuk mendeteksi metode permintaan tertentu (seperti OPTIONS untuk request preflight) atau untuk menyaring nilai header Origin yang 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:

  1. Skema (Protocol): Misalnya http atau https.
  2. Inang (Host/Domain): Misalnya example.com atau api.example.com.
  3. Port: Misalnya 80, 443, atau 8080.

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 MemintaKomponen yang BerbedaKategori Hubunganstatus Akses SOP Bawaan
https://api.example.com/v1/profilesTidak Ada (Port default HTTPS adalah 443)Same OriginDiizinkan (Akses Penuh)
http://api.example.com/v1/usersSkema (http vs https)Cross OriginDiblokir oleh SOP
https://api.example.com:8443/v1/usersPort (8443 vs 443)Cross OriginDiblokir oleh SOP
https://www.example.com/v1/usersHost (www.example.com vs api.example.com)Cross OriginDiblokir oleh SOP
https://example.com/v1/usersHost (example.com vs api.example.com)Cross OriginDiblokir 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, atau HEAD.
  • Hanya menggunakan header bawaan aman seperti Accept, Accept-Language, Content-Language, dan Content-Type.
  • Nilai header Content-Type yang diperbolehkan hanya terbatas pada: application/x-www-form-urlencoded, multipart/form-data, atau text/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: true

Dalam 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 request OPTIONS lagi 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 @options yang hanya akan mencocokkan request dengan metode HTTP OPTIONS. 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 port 8080 tidak dibebani oleh permintaan OPTIONS. 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:

  1. Wajib menyertakan Access-Control-Allow-Credentials: true pada respons dari server. Jika header ini tidak ada atau bernilai false, browser akan menolak memberikan respons data ke frontend kita.
  2. JANGAN sekali-kali menggunakan wildcard * pada header Access-Control-Allow-Origin. Jika server mengembalikan Access-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: Origin ketika menggunakan domain dinamis. Jika kita tidak menyertakan header Vary: Origin, server caching di tengah jalan (seperti CDN, CDN Cloudflare, atau proxy browser) mungkin akan meng-cache respons dengan header Access-Control-Allow-Origin: https://dashboard.example.com untuk pengunjung pertama. Ketika pengunjung berikutnya datang menggunakan origin https://admin.example.com, CDN akan menyajikan halaman yang di-cache tersebut. Browser pengunjung kedua akan menolak respons karena mendeteksi ketidakcocokan origin. Header Vary: Origin memberitahu server caching untuk meng-cache respons secara terpisah untuk setiap nilai header Origin request 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 AnalisisAPI 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-OriginWildcard *Origin spesifik (statis atau dinamis)
Tingkat Risiko KeamananSangat Rendah (Data bersifat terbuka)Sangat Tinggi (Akses data sensitif)
Kebutuhan Header VaryTidak Butuh (Sama untuk semua origin)Sangat Wajib (Vary: Origin untuk caching)
Metode HTTP yang DiizinkanUmumnya hanya GET dan OPTIONSSemua 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:

  1. 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.
  2. 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.
  3. 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-Origin sesuai panduan di atas.
  • 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 (http vs https), dan port secara teliti.
  • 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, direktif header standar tidak akan dieksekusi sehingga header CORS hilang dari respons.
    • Solusi: Gunakan kata kunci defer pada direktif header di Caddyfile. Opsi defer menginstruksikan Caddy untuk menambahkan header tepat sebelum respons ditulis ke jaringan, apa pun status kode HTTP yang dihasilkan oleh backend.
# 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 OPTIONS dibebaskan 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 header dan matcher — tidak ada modul eksternal yang perlu dipasang.
  • OPTIONS Preflight Request: Harus selalu dipotong secara langsung di tingkat Caddy menggunakan matcher @options method OPTIONS dan respons kosong 204 No Content agar menghemat beban pemrosesan aplikasi backend kita.
  • Keamanan Kredensial: Saat request frontend menyertakan cookies atau token otentikasi kustom, Access-Control-Allow-Credentials wajib disetel ke true, dan nilai Access-Control-Allow-Origin tidak boleh menggunakan wildcard *.
  • Daftar Izin Dinamis (Dynamic Whitelist): Kita dapat mencocokkan header request Origin klien menggunakan matcher Caddy, kemudian mengembalikan nilainya secara dinamis melalui placeholder {header.Origin}.
  • Vary Header: Selalu tambahkan header respons Vary: Origin saat 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 defer pada direktif header di Caddy agar header respons CORS tidak terhapus.

← Sebelumnya: Security Headers   Berikutnya: Middleware Rewrite →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact