Error Pages #

Halaman kesalahan (error pages) yang dirancang dengan profesional adalah elemen krusial dalam membangun pengalaman pengguna (user experience atau UX) yang matang pada sebuah website. Ketika pengunjung menemukan tautan yang rusak (broken link) atau ketika server mengalami kendala teknis di latar belakang, menampilkan pesan kesalahan mentah bawaan browser (seperti layar putih polos berisi tulisan “Internal Server Error”) dapat membuat pengunjung bingung dan segera meninggalkan situs kita. Halaman kesalahan yang ramah pengguna mampu memandu pengunjung untuk kembali ke jalur navigasi yang benar, menyarankan halaman alternatif, atau menyediakan kontak bantuan.

Caddy menyediakan mesin penanganan kesalahan yang sangat kuat melalui direktif handle_errors. Modul ini memungkinkan Caddy untuk mencegat respons kesalahan (baik status kode 4xx maupun 5xx), menghentikan proses perutean biasa, dan mengalihkan lalu lintas secara internal untuk menyajikan halaman kesalahan kustom. Artikel ini akan membahas secara mendalam cara kerja handle_errors, variabel placeholder kesalahan yang dapat kita gunakan, teknik perancangan halaman kesalahan dinamis, serta integrasi pemantauan kesalahan menggunakan kode pelacakan (trace ID).


Cara Kerja Blok handle_errors di Caddy #

Satu hal krusial yang membedakan penanganan kesalahan Caddy dengan server web tradisional adalah bahwa handle_errors tidak melakukan pengalihan HTTP 302/307 eksternal ke browser. Proses pencegatan berjalan sepenuhnya di dalam memori Caddy:

flowchart TD
    Client["Permintaan Klien"] --> MainHandler["Handler Utama"]
    MainHandler --> Error["Terjadi Kesalahan (404 / 500 / 502)"]
    Error --> Eval["Evaluasi handle_errors"]
    Eval --> Send["Kirim Respon"]
    Send --> Browser["Browser Tetap di URL Asli<br/>(Status asli tetap dipertahankan untuk SEO)"]

Karena perutean ulang berjalan secara internal, browser pengunjung tetap berada di URL yang mereka ketik sebelumnya dan menerima kode status asli (seperti 404 atau 500) bersama dengan dokumen HTML kustom kita. Hal ini sangat penting untuk kepatuhan SEO; jika server mengalihkan error 404 ke URL /404.html dengan status HTTP 200, mesin pencari seperti Google akan mengindeks halaman error tersebut sebagai konten normal, yang dapat merusak peringkat pencarian situs kita.


Konfigurasi Dasar #

Untuk mengaktifkan halaman kesalahan kustom, kita mendeklarasikan blok handle_errors di bagian akhir blok situs kita. Caddyfile dasar di bawah ini menunjukkan cara menyajikan file HTML kustom berdasarkan status kode kesalahan:

example.com {
    root * /var/www/html
    file_server
    
    # Blok penanganan kesalahan
    handle_errors {
        # 1. Alihkan direktori dasar ke folder khusus halaman kesalahan
        root * /var/www/errors
        
        # 2. Tulis ulang semua URI permintaan menjadi nama berkas status kode (misal: /404.html)
        rewrite * /{err.status_code}.html
        
        # 3. Sajikan berkas statis dari root yang baru
        file_server
    }
}

Rekomendasi Struktur Direktori Berkas Kesalahan: #

Di server kita, kita dapat membuat folder khusus /var/www/errors/ untuk menyimpan berbagai variasi tampilan halaman kesalahan:

/var/www/errors/
  ├── 400.html    # Permintaan Buruk (Bad Request)
  ├── 401.html    # Tidak Terotentikasi (Unauthorized)
  ├── 403.html    # Akses Dilarang (Forbidden)
  ├── 404.html    # Halaman Tidak Ditemukan (Not Found)
  ├── 429.html    # Terlalu Banyak Permintaan (Rate Limited)
  ├── 500.html    # Kesalahan Server Internal (Internal Server Error)
  ├── 502.html    # Gateway Buruk / Backend Mati (Bad Gateway)
  └── 503.html    # Layanan Tidak Tersedia / Maintenance (Service Unavailable)

Placeholder Kesalahan yang Disediakan Caddy #

Ketika berada di dalam blok handle_errors, Caddy menyuntikkan beberapa variabel placeholder khusus yang berisi informasi rinci mengenai kesalahan yang baru saja terjadi. Kita dapat menggunakan variabel ini untuk merender halaman kesalahan secara dinamis atau mencatatnya ke dalam log:

PlaceholderTipe NilaiDeskripsi
{err.status_code}IntegerKode status HTTP kesalahan (seperti 404, 403, 500, 502).
{err.status_text}StringDeskripsi singkat standar HTTP (seperti "Not Found", "Internal Server Error").
{err.message}StringPesan kesalahan terperinci dari modul internal Caddy (berguna untuk analisis admin).
{err.trace}StringID pelacakan unik (trace ID) untuk korelasi dengan berkas log server.
{err.id}StringID unik kesalahan yang dihasilkan oleh modul HTTP.

Pemisahan Logika Halaman Kesalahan Berdasarkan Kode Status #

Dalam skenario dunia nyata, kita ingin menampilkan pesan yang sangat berbeda untuk kesalahan di sisi klien (4xx) dan kesalahan di sisi server (5xx). Kesalahan 4xx biasanya karena kelalaian pengguna mengetik URL, sedangkan kesalahan 5xx adalah tanggung jawab pengelola server.

Kita dapat menggunakan ekspresi pencocokan (expression matchers) di dalam blok handle_errors untuk memisahkan logika penanganannya:

example.com {
    root * /var/www/html
    file_server
    
    handle_errors {
        # Mendefinisikan ekspresi pencocokan berdasarkan status kode
        @404 expression {err.status_code} == 404
        @5xx expression {err.status_code} >= 500
        
        # 1. Penanganan Halaman 404 Khusus
        handle @404 {
            root * /var/www/html
            rewrite * /404.html
            file_server
        }
        
        # 2. Penanganan Halaman Kesalahan Server (5xx)
        handle @5xx {
            root * /var/www/errors
            # Gunakan template 500 terpadu
            rewrite * /500.html
            file_server
        }
        
        # 3. Penanganan Default untuk Status Kode Lainnya (misal: 403, 401)
        handle {
            respond "Kesalahan Sistem ({err.status_code}): {err.status_text}" {err.status_code}
        }
    }
}

Merancang Halaman Kesalahan Dinamis Menggunakan Direktif templates #

Alih-alih membuat puluhan file HTML statis yang berbeda untuk setiap status kode, kita dapat membuat satu file HTML dinamis menggunakan fitur template Caddy. Direktif templates memerintahkan Caddy untuk memproses berkas HTML menggunakan mesin template Go sebelum mengirimkannya ke pengguna, memungkinkan kita menggunakan fungsi placeholder untuk menampilkan data secara dinamis.

Konfigurasi Caddyfile: #

example.com {
    root * /var/www/html
    file_server
    
    handle_errors {
        root * /var/www/errors
        # Aktifkan mesin template
        templates
        rewrite * /dynamic-error.html
        file_server
    }
}

Berkas HTML Template Dinamis (dynamic-error.html): #

Berikut adalah implementasi halaman kesalahan modern, responsif, menggunakan latar belakang gradien yang menawan, serta menampilkan pesan yang disesuaikan secara dinamis berdasarkan kode status kesalahan:

<!-- /var/www/errors/dynamic-error.html -->
<!DOCTYPE html>
<html lang="id">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Kesalahan {{placeholder "err.status_code"}} — {{placeholder "err.status_text"}}</title>
    <style>
        :root {
            --primary: #4f46e5;
            --primary-hover: #4338ca;
            --bg-gradient: linear-gradient(135deg, #e0e7ff 0%, #c7d2fe 100%);
            --card-bg: #ffffff;
            --text-main: #1e293b;
            --text-muted: #64748b;
        }
        @media (prefers-color-scheme: dark) {
            :root {
                --primary: #6366f1;
                --primary-hover: #4f46e5;
                --bg-gradient: linear-gradient(135deg, #0f172a 0%, #1e1b4b 100%);
                --card-bg: #1e293b;
                --text-main: #f8fafc;
                --text-muted: #94a3b8;
            }
        }
        * { box-sizing: border-box; margin: 0; padding: 0; }
        body {
            font-family: system-ui, -apple-system, sans-serif;
            background: var(--bg-gradient);
            color: var(--text-main);
            min-height: 100vh;
            display: flex;
            align-items: center;
            justify-content: center;
            padding: 1.5rem;
        }
        .card {
            background-color: var(--card-bg);
            border-radius: 16px;
            padding: 3rem 2rem;
            max-width: 480px;
            width: 100%;
            text-align: center;
            box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.1), 0 8px 10px -6px rgba(0, 0, 0, 0.1);
        }
        .code {
            font-size: 5.5rem;
            font-weight: 800;
            color: var(--primary);
            line-height: 1;
            margin-bottom: 0.5rem;
        }
        .title {
            font-size: 1.5rem;
            font-weight: 700;
            margin-bottom: 1rem;
        }
        .message {
            color: var(--text-muted);
            font-size: 1rem;
            margin-bottom: 2rem;
            line-height: 1.6;
        }
        .actions {
            display: flex;
            gap: 1rem;
            justify-content: center;
        }
        .btn {
            display: inline-block;
            padding: 0.75rem 1.5rem;
            border-radius: 8px;
            font-weight: 600;
            text-decoration: none;
            transition: background-color 0.2s;
        }
        .btn-primary {
            background-color: var(--primary);
            color: #ffffff;
        }
        .btn-primary:hover { background-color: var(--primary-hover); }
        .btn-secondary {
            background-color: rgba(0,0,0,0.05);
            color: var(--text-main);
        }
        @media (prefers-color-scheme: dark) {
            .btn-secondary { background-color: rgba(255,255,255,0.05); }
        }
        .btn-secondary:hover { background-color: rgba(0,0,0,0.1); }
        .trace {
            margin-top: 2.5rem;
            font-size: 0.75rem;
            color: var(--text-muted);
            font-family: monospace;
            border-top: 1px solid var(--text-muted);
            padding-top: 1rem;
            opacity: 0.7;
        }
    </style>
</head>
<body>
    <div class="card">
        <div class="code">{{placeholder "err.status_code"}}</div>
        <div class="title">{{placeholder "err.status_text"}}</div>
        
        <div class="message">
            <!-- Evaluasi kondisi kustom per status kode -->
            {{if eq (placeholder "err.status_code") "404"}}
                Halaman yang kita cari tidak dapat ditemukan. Kemungkinan tautan tersebut telah kedaluwarsa atau alamat URL yang dimasukkan salah.
            {{else if eq (placeholder "err.status_code") "403"}}
                Akses ditolak. Kita tidak memiliki hak akses administratif untuk membaca folder atau berkas pada jalur ini.
            {{else if ge (placeholder "err.status_code") "500"}}
                Terjadi kendala pada sistem server kami. Tim infrastruktur telah kami kabarkan untuk melakukan perbaikan segera.
            {{else}}
                Terjadi kesalahan sistem yang tidak terduga. Silakan coba kembali beberapa saat lagi.
            {{end}}
        </div>
        
        <div class="actions">
            <a href="/" class="btn btn-primary">🏠 Ke Beranda</a>
            <a href="javascript:history.back()" class="btn btn-secondary">← Kembali</a>
        </div>
        
        <!-- Cetak Trace ID untuk mempermudah pelacakan admin jika terjadi error server internal -->
        {{if ge (placeholder "err.status_code") "500"}}
        <div class="trace">
            ID Lacak: {{placeholder "err.trace"}}
        </div>
        {{end}}
    </div>
</body>
</html>

Penanganan Kesalahan untuk Layanan Reverse Proxy (Gateway Failures) #

Saat Caddy bertindak sebagai reverse proxy di depan server aplikasi (seperti Node.js, Go backend, PHP-FPM, atau Python/Django), kegagalan dapat terjadi pada dua tingkat:

  1. Kesalahan dari Caddy (Gateway level): Backend aplikasi mati total, sehingga Caddy tidak dapat terhubung (connection refused) dan terpaksa melahirkan status kode 502 Bad Gateway.
  2. Kesalahan dari Aplikasi: Backend hidup, namun kode aplikasi mengalami crash dan mengirimkan respons status kode 500 ke Caddy.

Secara default, Caddy akan meneruskan respons 500 dari backend apa adanya tanpa interseptasi. Namun, kita dapat mengonfigurasi Caddy untuk mencegat kesalahan dari backend maupun kegagalan koneksi Caddy sendiri untuk diseragamkan penampilannya menggunakan handle_errors:

app.example.com {
    # Mencegat kesalahan koneksi 502/503/504 dari proxy
    handle_errors {
        @gateway_error expression {err.status_code} in [502, 503, 504]
        
        handle @gateway_error {
            root * /var/www/errors
            rewrite * /maintenance.html
            file_server
        }
        
        # Penanganan kesalahan umum lainnya
        handle {
            root * /var/www/errors
            rewrite * /generic-error.html
            file_server
        }
    }
    
    # Konfigurasi reverse proxy utama
    reverse_proxy localhost:3000 {
        # Mengaktifkan deteksi kegagalan koneksi
        health_uri /healthz
        health_interval 10s
    }
}

Pola Maintenance Mode Terpadu #

Ketika tim infrastruktur kita perlu melakukan pemeliharaan server berkala (scheduled maintenance), kita dapat memotong seluruh lalu lintas dan mengarahkannya ke halaman pemeliharaan kustom menggunakan status kode 503:

example.com {
    # Aktifkan baris di bawah saat proses pemeliharaan berlangsung
    # respond "Pemeliharaan Sistem" 503
    
    root * /var/www/html
    file_server
    
    handle_errors {
        @maintenance expression {err.status_code} == 503
        handle @maintenance {
            root * /var/www/maintenance-pages
            rewrite * /index.html
            file_server
        }
    }
}

Integrasi Trace ID dengan Log Server #

Ketika pengguna melaporkan kesalahan “500 Internal Server Error”, sangat sulit melacak penyebab pastinya tanpa mengetahui baris log mana yang mencatat error tersebut. Caddy mempermudah korelasi ini menggunakan Trace ID ({err.trace}).

Caddy secara otomatis mencatat Trace ID ini ke dalam berkas log akses JSON. Di dalam halaman kesalahan kita (seperti contoh template dinamis di atas), kita mencetak ID Lacak tersebut ke layar. Ketika pengguna melaporkan masalah ke tim dukungan teknis (technical support), mereka cukup menyalin string ID Lacak tersebut.

Admin sistem kita kemudian dapat mencari penyebab pastinya di server secara instan:

# Mencari penyebab kesalahan spesifik di log menggunakan grep Trace ID
grep "string-trace-id-dari-pengguna" /var/log/caddy/access.log

Respons log JSON akan menunjukkan secara tepat jenis kesalahan internal, alamat IP hulu (upstream), serta status database saat koneksi bermasalah.


Ringkasan #

  • Interseptasi Internal — Direktif handle_errors memotong respons kesalahan dan mengalihkan alur proses secara internal tanpa mengubah URL di browser (menjaga kesehatan SEO).
  • Variabel Pelacakan — Placeholder {err.status_code} dan {err.trace} sangat berguna untuk menyajikan informasi dinamis dan memfasilitasi pelacakan log admin.
  • Template Dinamis — Kombinasi direktif templates dan fungsi placeholder di HTML memungkinkan kita merancang satu berkas halaman kesalahan terpadu untuk semua kode status.
  • Reverse Proxy Failure — Caddy dapat mencegat kesalahan gateway (seperti 502/503 saat backend mati) untuk menyajikan halaman pemeliharaan kustom ke pengunjung.
  • Penyelaras Log — Mencetak Trace ID pada halaman kesalahan server (5xx) membantu mempercepat proses perbaikan dengan mencocokkan ID tersebut pada log akses JSON server.

← Sebelumnya: Directory Browse   Berikutnya: Konsep Reverse Proxy →

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