Custom Error Page #
Halaman error bawaan Nginx sangat minimalis — teks putih di layar dengan kode status dan tulisan “nginx/1.x.x”. Untuk website production, kita hampir pasti ingin menggantinya: halaman error kustom yang sesuai tampilan brand, memberikan informasi yang berguna bagi pengguna, dan yang paling penting, tidak mengekspos versi server kita kepada penyerang.
Artikel ini akan membahas error_page secara mendalam: cara kerjanya, berbagai pola konfigurasi, menangani error dari backend, dan implementasi production-ready.
Cara Kerja error_page #
error_page mendefinisikan apa yang dikembalikan Nginx ketika respons memiliki kode status tertentu. Directive ini melakukan internal redirect — Nginx seolah-olah menerima request baru untuk URI yang ditentukan, lalu menyervisnya.
sequenceDiagram
participant B as Browser
participant N as Nginx
participant F as Filesystem
B->>N: GET /page-yang-tidak-ada HTTP/1.1
N->>F: Cari file: /var/www/html/page-yang-tidak-ada
F-->>N: Tidak ada (ENOENT)
Note over N: Status 404 terpicu\nerror_page 404 /404.html
N->>N: Internal redirect ke GET /404.html
N->>F: Cari file: /var/www/html/404.html
F-->>N: File ada
N->>B: HTTP/1.1 404 Not Found\nContent-Type: text/html\n[konten 404.html]Poin kunci: browser tetap menerima kode status 404 — bukan 200. Nginx hanya mengubah konten yang dikembalikan, bukan kode statusnya (kecuali kita secara eksplisit mengubahnya, yang akan dibahas nanti).
Sintaks Dasar #
server {
root /var/www/html;
# Satu kode status → satu file
error_page 404 /404.html;
# Beberapa kode status → satu file yang sama
error_page 500 502 503 504 /50x.html;
# Lindungi file error dari akses langsung
# (pengguna tidak bisa akses http://example.com/404.html langsung)
location = /404.html {
internal;
}
location = /50x.html {
internal;
}
}
Directive internal pada location block error page mencegah pengguna mengakses file tersebut secara langsung dari browser. Tanpa internal, seseorang bisa mengakses http://example.com/404.html dan mendapatkan respons 200 OK — yang membingungkan dan bermasalah untuk SEO.
Menyimpan Error Page di Direktori Terpisah #
Untuk memudahkan manajemen, simpan semua halaman error di satu direktori khusus:
server {
root /var/www/html;
# Semua error page ditangani di sini
error_page 400 /errors/400.html;
error_page 401 /errors/401.html;
error_page 403 /errors/403.html;
error_page 404 /errors/404.html;
error_page 405 /errors/405.html;
error_page 408 /errors/408.html;
error_page 429 /errors/429.html;
error_page 500 502 503 504 /errors/50x.html;
# Satu location untuk semua file error
# File ada di /var/www/html/errors/
location ^~ /errors/ {
internal; # Hanya bisa diakses sebagai internal redirect
root /var/www/html;
# Path: root + URI = /var/www/html + /errors/404.html
}
# Atau jika file error ada di lokasi terpisah dari web root:
location ^~ /errors/ {
internal;
alias /opt/nginx-error-pages/;
# Path: alias + (URI setelah /errors/) = /opt/nginx-error-pages/404.html
}
}
Mengubah Kode Status Respons #
Secara default, error_page mempertahankan kode status asli. Tapi kita bisa mengubahnya menggunakan operator =:
server {
# ─── Default: tampilkan halaman 404, kembalikan status 404 ────────────────
error_page 404 /404.html;
# ─── Tampilkan halaman tapi ganti kode status ─────────────────────────────
# Ubah 404 → 200 (berguna untuk SPA yang butuh semua path
# mengembalikan index.html dengan status 200)
error_page 404 =200 /index.html;
# Ubah 403 → 404 (menyembunyikan keberadaan resource yang terlarang)
# Penyerang tidak tahu apakah resource ada tapi terlarang, atau tidak ada sama sekali
error_page 403 =404 /404.html;
# Ubah error server ke 503 Service Unavailable
# (berguna saat maintenance)
error_page 500 502 504 =503 /maintenance.html;
}
Kasus Khusus: SPA dengan error_page #
Framework SPA seperti React, Vue, dan Angular menangani routing di sisi client. Semua path (termasuk yang “tidak ada” dari perspektif server) harus mengembalikan index.html:
server {
root /var/www/spa;
location / {
try_files $uri $uri/ /index.html;
# Cara yang lebih direkomendasikan untuk SPA
}
# Alternatif menggunakan error_page (kurang direkomendasikan):
error_page 404 =200 /index.html;
# Masalah: semua 404 akan jadi 200, termasuk missing asset (CSS, JS, gambar)
# yang seharusnya tetap 404 agar browser tidak salah cache
}
Error dari Backend: proxy_intercept_errors #
Ketika Nginx digunakan sebagai reverse proxy, error bisa datang dari backend (aplikasi Node.js, Python, PHP, dsb). Secara default, Nginx meneruskan halaman error dari backend langsung ke browser.
Dengan proxy_intercept_errors on, Nginx mengambil alih dan menampilkan halaman error kustom kita:
server {
location / {
proxy_pass http://backend;
# Aktifkan intercepting error dari backend
proxy_intercept_errors on;
# Sekarang error_page di server ini berlaku untuk error backend juga
error_page 502 503 504 /maintenance.html;
error_page 500 /500.html;
}
location = /maintenance.html {
root /var/www/html;
internal;
}
location = /500.html {
root /var/www/html;
internal;
}
}
flowchart LR
B["Browser"] --> N["Nginx\nReverse Proxy"]
N --> BK["Backend\nNode.js, Python, atau PHP"]
BK -- "Error 502 dari backend" --> N
N -- "proxy_intercept_errors on\nError 502 di-intercept" --> EP["Ambil /maintenance.html\nKembalikan ke browser"]
EP --> BPerbedaan dengan dan tanpa proxy_intercept_errors #
Tanpa proxy_intercept_errors:
Browser ← Nginx ← Backend mengirim halaman error sendiri (template error Express.js, dsb)
Dengan proxy_intercept_errors on:
Browser ← Nginx menampilkan /maintenance.html kita
(halaman error backend diabaikan)
Named Location sebagai Error Handler #
Untuk logika error yang lebih kompleks, kita bisa menggunakan named location:
server {
location / {
proxy_pass http://backend;
proxy_intercept_errors on;
error_page 502 503 @maintenance;
error_page 500 @server_error;
}
location @maintenance {
root /var/www/html;
try_files /maintenance.html =503;
# Set header tambahan
add_header Retry-After 3600;
add_header Cache-Control "no-store";
}
location @server_error {
root /var/www/html;
try_files /500.html =500;
# Log error yang tidak terduga
access_log /var/log/nginx/500-errors.log;
}
}
Redirect ke URL Eksternal saat Error #
error_page bisa juga redirect ke URL eksternal, meskipun ini akan mengubah kode status menjadi redirect (301/302):
# Redirect ke halaman status eksternal
error_page 503 https://status.example.com;
# Catatan: ini akan mengembalikan 302 ke browser,
# bukan 503. Browser mengikuti redirect.
# Lebih baik: gunakan internal redirect ke file lokal
# yang bisa mengambil konten dari halaman status jika perlu
Menyembunyikan Versi Nginx #
Halaman error bawaan Nginx secara default menampilkan versi:
<hr><center>nginx/1.24.0</center>
Ini memberikan informasi yang tidak perlu kepada penyerang — mereka bisa mencari CVE yang spesifik untuk versi tersebut. Matikan dengan server_tokens:
http {
# Sembunyikan versi Nginx dari:
# - Header "Server" di setiap response
# - Halaman error bawaan Nginx
server_tokens off;
# Dengan server_tokens off:
# Header: Server: nginx (tanpa versi)
# Error page: nginx (tanpa versi)
}
Jika ingin menyembunyikan sepenuhnya bahwa server menggunakan Nginx (bukan hanya versinya):
# Dengan Nginx Plus atau modul nginx-headers-more:
more_set_headers 'Server: MyApp';
# Tanpa modul tambahan, kita hanya bisa hide versi, bukan nama server
# (kecuali compile Nginx dengan custom build)
Template Halaman Error HTML #
Berikut contoh template halaman error yang profesional, responsif, dan informative:
404 Not Found #
<!-- /var/www/html/errors/404.html -->
<!DOCTYPE html>
<html lang="id">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>404 — Halaman Tidak Ditemukan</title>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
background: #f8fafc;
color: #334155;
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
padding: 2rem;
}
.container {
text-align: center;
max-width: 500px;
}
.code {
font-size: 8rem;
font-weight: 900;
color: #e2e8f0;
line-height: 1;
margin-bottom: 1rem;
}
h1 { font-size: 1.5rem; margin-bottom: 0.75rem; }
p { color: #64748b; margin-bottom: 2rem; line-height: 1.6; }
.btn {
display: inline-block;
background: #3b82f6;
color: white;
padding: 0.75rem 2rem;
border-radius: 8px;
text-decoration: none;
font-weight: 600;
transition: background 0.2s;
}
.btn:hover { background: #2563eb; }
</style>
</head>
<body>
<div class="container">
<div class="code">404</div>
<h1>Halaman tidak ditemukan</h1>
<p>Halaman yang kamu cari mungkin telah dipindahkan, dihapus, atau URL yang kamu ketik tidak tepat.</p>
<a href="/" class="btn">Kembali ke Beranda</a>
</div>
</body>
</html>
50x Server Error #
<!-- /var/www/html/errors/50x.html -->
<!DOCTYPE html>
<html lang="id">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Terjadi Kesalahan — Coba Lagi Nanti</title>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
background: #fff7f7;
color: #334155;
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
padding: 2rem;
}
.container { text-align: center; max-width: 500px; }
.icon { font-size: 4rem; margin-bottom: 1.5rem; }
h1 { font-size: 1.5rem; margin-bottom: 0.75rem; color: #dc2626; }
p { color: #64748b; margin-bottom: 1rem; line-height: 1.6; }
.btn {
display: inline-block;
background: #dc2626;
color: white;
padding: 0.75rem 2rem;
border-radius: 8px;
text-decoration: none;
font-weight: 600;
}
</style>
</head>
<body>
<div class="container">
<div class="icon">⚠️</div>
<h1>Terjadi kesalahan pada server</h1>
<p>Server mengalami masalah sementara dan tidak dapat memproses permintaanmu sekarang.</p>
<p>Tim kami sudah mendapat notifikasi dan sedang menangani masalah ini. Coba lagi dalam beberapa menit.</p>
<a href="javascript:location.reload()" class="btn">Coba Lagi</a>
</div>
</body>
</html>
Konfigurasi error_page Production Lengkap #
Berikut adalah konfigurasi error_page yang komprehensif untuk server production:
server {
listen 443 ssl;
http2 on;
server_name example.com;
root /var/www/example.com;
# ─── Keamanan: sembunyikan versi Nginx ────────────────────────────────────
server_tokens off;
# ─── Error pages untuk berbagai kode status ───────────────────────────────
error_page 400 /errors/400.html; # Bad Request
error_page 401 /errors/401.html; # Unauthorized
error_page 403 /errors/403.html; # Forbidden
error_page 404 /errors/404.html; # Not Found
error_page 408 /errors/408.html; # Request Timeout
error_page 429 /errors/429.html; # Too Many Requests (rate limiting)
error_page 500 /errors/500.html; # Internal Server Error
error_page 502 /errors/502.html; # Bad Gateway (backend down)
error_page 503 /errors/503.html; # Service Unavailable (maintenance)
error_page 504 /errors/504.html; # Gateway Timeout
# ─── Location untuk semua error pages ────────────────────────────────────
location ^~ /errors/ {
internal;
root /var/www/example.com;
# File ada di /var/www/example.com/errors/404.html dsb.
}
# ─── Main routing ─────────────────────────────────────────────────────────
location / {
proxy_pass http://backend;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# Intercept error dari backend dan tampilkan halaman kustom kita
proxy_intercept_errors on;
# Timeout yang wajar
proxy_connect_timeout 5s;
proxy_read_timeout 60s;
proxy_send_timeout 60s;
}
# ─── Maintenance mode: aktifkan dengan flag file ──────────────────────────
# Buat file /var/www/example.com/maintenance.flag untuk masuk maintenance mode
set $maintenance 0;
if (-f $document_root/maintenance.flag) {
set $maintenance 1;
}
location / {
if ($maintenance = 1) {
return 503;
}
proxy_pass http://backend;
proxy_intercept_errors on;
error_page 503 /errors/503.html;
}
}
Debugging error_page yang Tidak Berjalan #
Ada beberapa kasus yang membuat error_page tidak berjalan seperti yang diharapkan:
Kasus 1: Recursion Protection #
Jika file error page itu sendiri menghasilkan error, Nginx tidak akan infinite loop — tapi juga tidak akan menampilkan error page lebih lanjut:
# Cek apakah file error page ada dan bisa dibaca
ls -la /var/www/html/errors/404.html
cat /var/www/html/errors/404.html | head -5
# Cek error log untuk "open() ... failed"
tail -f /var/log/nginx/error.log
Kasus 2: error_page Tidak Berlaku untuk Backend Error #
# SALAH: proxy_intercept_errors tidak aktif
location / {
proxy_pass http://backend;
error_page 502 /maintenance.html; # Tidak akan berjalan!
}
# BENAR: proxy_intercept_errors harus aktif
location / {
proxy_pass http://backend;
proxy_intercept_errors on; # ← Ini yang mengaktifkan
error_page 502 /maintenance.html;
}
Kasus 3: File error_page Tidak Ada #
# Nginx akan log error dan mungkin menampilkan error bawaan
# jika file yang direferensikan di error_page tidak ada:
# [error] open() "/var/www/html/404.html" failed (2: No such file or directory)
Kasus 4: Konflik dengan try_files #
location / {
# try_files mengembalikan =404 secara internal
# ini BUKAN error HTTP 404 yang biasa — ini adalah "pseudo-request"
# yang di-handle secara berbeda
try_files $uri $uri/ =404;
# error_page 404 AKAN terpicu oleh ini
}
Ringkasan #
error_page 404 /404.html— directive dasar untuk menentukan halaman error kustom per kode status. Nginx melakukan internal redirect ke URI tersebut.- Selalu tambahkan
internal;di location block yang melayani error page — mencegah akses langsung dari browser.proxy_intercept_errors ondiperlukan agar Nginx menampilkan halaman error kustom saat backend mengembalikan error — tanpanya, halaman error dari backend yang diteruskan langsung.server_tokens offdihttpcontext untuk menyembunyikan versi Nginx dari headerServerdan halaman error bawaan — wajib di production.error_page 403 =404untuk menyembunyikan keberadaan resource terlarang — penyerang tidak bisa membedakan apakah resource tidak ada atau ada tapi diblokir.error_page 404 =200 /index.htmluntuk SPA, tapitry_files $uri $uri/ /index.htmllebih direkomendasikan karena lebih spesifik (hanya non-existent paths, bukan semua 404).- Gunakan
^~ /errors/+internaluntuk mengelola semua error page dalam satu direktori.
← Sebelumnya: Index & Autoindex Berikutnya: Konsep Reverse Proxy →