Error Pages

Error Pages #

Error page yang baik adalah bagian penting dari user experience website yang profesional. Sebuah halaman 404 yang dirancang dengan baik bisa membantu pengunjung menemukan konten yang mereka cari, alih-alih hanya menampilkan pesan error mentah yang membingungkan.

Caddy menyediakan handle_errors — sebuah blok khusus yang dieksekusi ketika terjadi error, memberikan kamu kontrol penuh atas apa yang ditampilkan ketika request gagal.

Cara Kerja handle_errors #

handle_errors adalah blok yang berjalan ketika handler utama mengembalikan error atau status code tertentu. Ia memiliki akses ke informasi tentang error yang terjadi dan bisa menampilkan response apapun:

Request → Handler utama → Error terjadi (404, 500, dll.)
                              │
                              ▼
                       Caddy masuk ke handle_errors
                              │
                              ▼
              Response kustom dikirim ke client

Konfigurasi Dasar #

example.com {
    root * /var/www/html
    file_server
    
    handle_errors {
        # Sajikan file error HTML dari direktori /var/www/errors
        root * /var/www/errors
        rewrite * /{err.status_code}.html
        file_server
    }
}

Dengan konfigurasi ini:

  • Error 404 → sajikan /var/www/errors/404.html
  • Error 500 → sajikan /var/www/errors/500.html
  • Error 403 → sajikan /var/www/errors/403.html

Struktur Direktori Error Pages #

/var/www/errors/
  ├── 400.html    ← Bad Request
  ├── 401.html    ← Unauthorized
  ├── 403.html    ← Forbidden
  ├── 404.html    ← Not Found
  ├── 408.html    ← Request Timeout
  ├── 429.html    ← Too Many Requests
  ├── 500.html    ← Internal Server Error
  ├── 502.html    ← Bad Gateway
  ├── 503.html    ← Service Unavailable
  └── 504.html    ← Gateway Timeout

Placeholder Error yang Tersedia #

Di dalam handle_errors, kamu bisa menggunakan placeholder khusus yang berisi informasi tentang error:

{err.status_code}    → HTTP status code (404, 500, dll.)
{err.status_text}    → Text deskripsi status ("Not Found", "Internal Server Error")
{err.message}        → Pesan error detail
{err.trace}          → Error trace ID (untuk korelasi dengan log)
{err.id}             → Error ID unik

Error Page Berbeda per Status Code #

example.com {
    root * /var/www/html
    file_server
    
    handle_errors {
        @404 expression {err.status_code} == 404
        @5xx expression {err.status_code} >= 500

        # Halaman 404 khusus
        handle @404 {
            root * /var/www/html
            rewrite * /404.html
            file_server
        }
        
        # Halaman generic untuk 5xx error
        handle @5xx {
            root * /var/www/html
            rewrite * /500.html
            file_server
        }
        
        # Default untuk error lainnya
        handle {
            respond "Error {err.status_code}: {err.status_text}" {err.status_code}
        }
    }
}

Error Page dengan Template Dinamis #

Caddy bisa menggunakan templates directive untuk merender halaman error dengan data dinamis dari placeholder:

example.com {
    root * /var/www/html
    file_server
    
    handle_errors {
        root * /var/www/errors
        templates
        rewrite * /error.html
        file_server
    }
}
<!-- /var/www/errors/error.html -->
<!DOCTYPE html>
<html lang="id">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Error {{placeholder "err.status_code"}} — Nama Situs</title>
    <style>
        * { margin: 0; padding: 0; box-sizing: border-box; }
        body {
            font-family: system-ui, -apple-system, sans-serif;
            background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
            min-height: 100vh;
            display: flex;
            align-items: center;
            justify-content: center;
        }
        .card {
            background: white;
            border-radius: 16px;
            padding: 3rem;
            text-align: center;
            max-width: 480px;
            width: 90%;
            box-shadow: 0 20px 60px rgba(0,0,0,0.3);
        }
        .status-code {
            font-size: 6rem;
            font-weight: 800;
            color: #6366f1;
            line-height: 1;
            margin-bottom: 0.5rem;
        }
        .status-text {
            font-size: 1.5rem;
            color: #1f2937;
            margin-bottom: 1rem;
        }
        .message {
            color: #6b7280;
            margin-bottom: 2rem;
            line-height: 1.6;
        }
        .actions { display: flex; gap: 1rem; justify-content: center; }
        .btn {
            padding: 0.75rem 1.5rem;
            border-radius: 8px;
            text-decoration: none;
            font-weight: 600;
            transition: opacity 0.2s;
        }
        .btn:hover { opacity: 0.85; }
        .btn-primary { background: #6366f1; color: white; }
        .btn-secondary { background: #f3f4f6; color: #374151; }
        .trace {
            margin-top: 2rem;
            font-size: 0.75rem;
            color: #9ca3af;
            font-family: monospace;
        }
    </style>
</head>
<body>
    <div class="card">
        <div class="status-code">{{placeholder "err.status_code"}}</div>
        <div class="status-text">{{placeholder "err.status_text"}}</div>
        
        <div class="message">
            {{if eq (placeholder "err.status_code") "404"}}
                Halaman yang kamu cari tidak ditemukan. Mungkin sudah dipindahkan
                atau URL yang kamu masukkan salah.
            {{else if eq (placeholder "err.status_code") "403"}}
                Kamu tidak memiliki izin untuk mengakses halaman ini.
            {{else if ge (placeholder "err.status_code") "500"}}
                Terjadi kesalahan di sisi server kami. Tim kami sudah diberitahu
                dan sedang bekerja untuk memperbaikinya.
            {{else}}
                Terjadi kesalahan yang tidak terduga. Silakan coba 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>
        
        {{if ge (placeholder "err.status_code") "500"}}
        <div class="trace">
            Error ID: {{placeholder "err.trace"}}
        </div>
        {{end}}
    </div>
</body>
</html>

Error Pages untuk Reverse Proxy #

Ketika Caddy digunakan sebagai reverse proxy, error bisa terjadi di dua level: Caddy sendiri (misal: backend tidak bisa dijangkau → 502) atau dari backend (misal: aplikasi return 500).

app.example.com {
    handle_errors {
        # Error 502/503/504: Backend tidak bisa dijangkau
        @backendDown expression {err.status_code} in [502, 503, 504]
        handle @backendDown {
            respond `<!DOCTYPE html>
<html>
<head><title>Maintenance</title></head>
<body>
<h1>🔧 Sedang dalam Pemeliharaan</h1>
<p>Aplikasi kami sedang dalam pemeliharaan singkat. Silakan coba lagi dalam beberapa menit.</p>
</body>
</html>` {err.status_code}
        }
        
        # Error 404 dari backend atau Caddy
        @notFound expression {err.status_code} == 404
        handle @notFound {
            root * /var/www/error-pages
            rewrite * /404.html
            file_server
        }
        
        # Semua error lain
        handle {
            root * /var/www/error-pages
            rewrite * /generic-error.html
            file_server
        }
    }
    
    reverse_proxy localhost:3000 {
        health_uri /health
        health_interval 10s
        
        # Jika semua upstream down, Caddy return 503
    }
}

Maintenance Mode #

Pola umum untuk menampilkan halaman maintenance:

example.com {
    # Aktifkan maintenance mode: uncomment baris di bawah
    # respond "Sedang maintenance" 503
    
    root * /var/www/html
    file_server
    
    handle_errors {
        @503 expression {err.status_code} == 503
        handle @503 {
            root * /var/www/maintenance
            rewrite * /index.html
            file_server
        }
    }
}

Error Logging dan Alerting #

app.example.com {
    log {
        output file /var/log/caddy/access.log
        format json
    }
    
    handle_errors {
        # Log error 5xx untuk monitoring
        @server_errors expression {err.status_code} >= 500
        
        handle @server_errors {
            # Catat ke log terpisah untuk alert
            # (Caddy otomatis log semua error ke access.log)
            respond `{"error": "{err.status_code}", "trace": "{err.trace}"}` {err.status_code}
        }
        
        handle {
            root * /var/www/errors
            rewrite * /{err.status_code}.html
            file_server
        }
    }
    
    reverse_proxy localhost:3000
}

Contoh Halaman 404 yang Baik #

<!-- /var/www/html/404.html — contoh halaman 404 yang membantu user -->
<!DOCTYPE html>
<html lang="id">
<head>
    <meta charset="UTF-8">
    <title>404 — Halaman Tidak Ditemukan</title>
    <style>
        body { font-family: system-ui, sans-serif; max-width: 600px;
               margin: 4rem auto; padding: 0 1rem; color: #333; }
        h1 { font-size: 3rem; color: #6366f1; }
        .suggestions { background: #f8f9ff; border-radius: 8px;
                       padding: 1.5rem; margin: 2rem 0; }
        .suggestions h2 { font-size: 1.1rem; margin-bottom: 1rem; }
        .suggestions ul { list-style: none; padding: 0; }
        .suggestions li { padding: 0.3rem 0; }
        .suggestions a { color: #6366f1; }
        .search-form { display: flex; gap: 0.5rem; margin-top: 1.5rem; }
        .search-form input { flex: 1; padding: 0.75rem; border: 1px solid #ddd;
                              border-radius: 6px; font-size: 1rem; }
        .search-form button { padding: 0.75rem 1.5rem; background: #6366f1;
                               color: white; border: none; border-radius: 6px;
                               cursor: pointer; }
    </style>
</head>
<body>
    <h1>404</h1>
    <p>Halaman yang kamu cari tidak ditemukan.</p>
    <p>URL mungkin sudah berubah atau halaman telah dihapus.</p>
    
    <div class="suggestions">
        <h2>Coba salah satu ini:</h2>
        <ul>
            <li><a href="/">🏠 Halaman Utama</a></li>
            <li><a href="/blog">📝 Blog</a></li>
            <li><a href="">📚 Dokumentasi</a></li>
            <li><a href="/kontak">📬 Hubungi Kami</a></li>
        </ul>
    </div>
    
    <form class="search-form" action="/search" method="get">
        <input type="text" name="q" placeholder="Cari di situs ini...">
        <button type="submit">Cari</button>
    </form>
</body>
</html>

Ringkasan #

  • handle_errors adalah blok khusus yang dieksekusi saat terjadi error — letakkan di akhir blok site, setelah semua handler lain.
  • Gunakan {err.status_code} dan {err.status_text} untuk membuat response yang spesifik per jenis error.
  • Untuk error page yang lebih kaya, gunakan templates directive dan file HTML dengan Go template syntax untuk menampilkan data error secara dinamis.
  • Pisahkan penanganan error 4xx (client errors) dari 5xx (server errors) — keduanya membutuhkan pesan yang berbeda untuk user.
  • Error 502/503/504 dari reverse proxy menandakan backend tidak bisa dijangkau — tampilkan halaman maintenance yang informatif.
  • Error page yang baik: sertakan link ke halaman lain yang berguna, form search, dan cara menghubungi support — jangan hanya tampilkan kode error saja.

← Sebelumnya: Directory Browse   Berikutnya: Konsep Reverse Proxy →

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