WebSocket #
WebSocket adalah protokol komunikasi dua arah berbasis TCP yang berjalan di atas HTTP. WebSocket dimulai sebagai HTTP request biasa, lalu di-upgrade menjadi koneksi persisten yang memungkinkan server mengirim data ke client kapan saja tanpa client harus request terlebih dahulu.
Kabar baiknya: Caddy mendukung WebSocket secara native tanpa konfigurasi tambahan. Saat Caddy mendeteksi request dengan header Upgrade: websocket, ia otomatis meneruskannya sebagai proxy WebSocket.
Cara Kerja WebSocket Upgrade #
1. Client kirim HTTP request dengan header Upgrade:
GET /chat HTTP/1.1
Host: example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Version: 13
2. Caddy forward request ke backend
3. Backend merespons dengan:
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
4. Koneksi berubah menjadi WebSocket — bidirectional, persisten
→ Client dan server bisa kirim pesan kapan saja
→ Koneksi tetap terbuka sampai salah satu pihak menutup
Konfigurasi Dasar #
example.com {
# Caddy otomatis handle WebSocket upgrade
# Tidak ada konfigurasi khusus yang diperlukan!
reverse_proxy localhost:3000
}
Caddy secara otomatis mendeteksi header Upgrade: websocket dan meneruskan koneksi WebSocket ke backend.
Node.js WebSocket Server (ws library) #
// server.js
const http = require('http');
const WebSocket = require('ws');
// HTTP server untuk health check
const server = http.createServer((req, res) => {
if (req.url === '/health') {
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ status: 'ok', connections: wss.clients.size }));
}
});
// WebSocket server pada HTTP server yang sama
const wss = new WebSocket.Server({ server, path: '/ws' });
wss.on('connection', (ws, req) => {
const clientIp = req.headers['x-real-ip'] || req.socket.remoteAddress;
console.log(`New connection from ${clientIp}`);
ws.on('message', (message) => {
console.log(`Received: ${message}`);
// Broadcast ke semua client
wss.clients.forEach((client) => {
if (client.readyState === WebSocket.OPEN) {
client.send(message.toString());
}
});
});
ws.on('close', () => {
console.log(`Client disconnected`);
});
ws.send(JSON.stringify({ type: 'welcome', message: 'Connected!' }));
});
server.listen(3000, '127.0.0.1', () => {
console.log('Server running on localhost:3000');
});
Socket.io dengan Caddy #
example.com {
encode gzip zstd
# Socket.io path — pastikan semua endpoint socket.io di-proxy
handle /socket.io/* {
reverse_proxy localhost:3000 {
header_up X-Real-IP {remote_host}
header_up X-Forwarded-Proto {scheme}
}
}
# Regular HTTP requests
handle /api/* {
reverse_proxy localhost:3000
}
# Static files
handle {
root * /var/www/myapp/public
file_server
}
}
// Socket.io server configuration
const io = require('socket.io')(server, {
// Jika Caddy memutus koneksi WebSocket terlalu cepat,
// turunkan pingTimeout dan gunakan polling sebagai fallback
transports: ['websocket', 'polling'],
pingTimeout: 60000,
pingInterval: 25000
});
WebSocket dengan Path-Based Routing #
example.com {
# Chat WebSocket
handle /ws/chat {
reverse_proxy chat-service:3001
}
# Notifications WebSocket
handle /ws/notifications {
reverse_proxy notification-service:3002
}
# Live updates WebSocket
handle /ws/updates {
reverse_proxy update-service:3003
}
# Regular HTTP API
handle /api/* {
reverse_proxy api-backend:8080
}
# Frontend
handle {
root * /var/www/html
file_server
}
}
Autentikasi WebSocket #
WebSocket tidak mendukung header custom setelah initial handshake. Autentikasi dilakukan saat upgrade request:
// Server: validasi token dari query parameter atau cookie
wss.on('connection', (ws, req) => {
// Opsi 1: Token dari query string
// ws://example.com/ws?token=JWT_TOKEN
const url = new URL(req.url, 'http://localhost');
const token = url.searchParams.get('token');
// Opsi 2: Token dari Cookie
// const cookies = parseCookies(req.headers.cookie);
// const token = cookies.auth_token;
if (!validateToken(token)) {
ws.close(1008, 'Unauthorized');
return;
}
// Authenticated connection
const user = decodeToken(token);
ws.userId = user.id;
ws.on('message', (message) => {
// Handle message dengan user context
});
});
example.com {
# Validasi auth di level Caddy sebelum WebSocket upgrade
# Memerlukan basicauth atau plugin auth
@ws_path path /ws/*
handle @ws_path {
basicauth {
alice $2a$14$aliceHashHere
}
reverse_proxy ws-backend:3000
}
handle {
reverse_proxy backend:3000
}
}
Load Balancing WebSocket dengan Sticky Sessions #
WebSocket connections bersifat stateful — satu client harus selalu terhubung ke server yang sama. Gunakan IP hash untuk sticky session:
example.com {
reverse_proxy {
to ws-server-1:3000 ws-server-2:3000 ws-server-3:3000
# IP hash memastikan client yang sama selalu ke server yang sama
lb_policy ip_hash
# Health check
health_uri /health
health_interval 10s
header_up X-Real-IP {remote_host}
header_up X-Forwarded-Proto {scheme}
}
}
Alternatif yang lebih baik untuk production: gunakan Redis sebagai shared state agar WebSocket bisa di-load balance tanpa sticky session.
Timeout Konfigurasi untuk WebSocket #
{
servers {
# Timeout untuk idle connections
# WebSocket yang aktif tidak akan terkena timeout ini
timeouts {
read_body 10s
read_header 10s
# Jangan set write timeout untuk WebSocket!
# write 0 (default, tidak ada timeout)
}
}
}
example.com {
reverse_proxy ws-backend:3000 {
transport http {
# Dial timeout (initial connection)
dial_timeout 5s
# Jangan set response_header_timeout untuk WebSocket
# WebSocket connections bisa berlangsung berhari-hari
# response_header_timeout tidak berlaku setelah upgrade
}
}
}
Debugging WebSocket #
# Test WebSocket dengan wscat
npm install -g wscat
# Koneksi ke WebSocket server via Caddy
wscat -c wss://example.com/ws
# Koneksi dengan header
wscat -c wss://example.com/ws \
-H "Authorization: Bearer token" \
-H "X-Custom: value"
# Test dengan curl (hanya untuk melihat response upgrade)
curl -iv \
--http1.1 \
-H "Connection: Upgrade" \
-H "Upgrade: websocket" \
-H "Sec-WebSocket-Version: 13" \
-H "Sec-WebSocket-Key: $(openssl rand -base64 16)" \
https://example.com/ws
# Cek apakah Caddy forward WebSocket headers dengan benar
# Lihat di access log: status 101 = WebSocket upgrade berhasil
cat /var/log/caddy/access.log | jq 'select(.status == 101)'
Ringkasan #
- Tidak ada konfigurasi khusus yang diperlukan untuk WebSocket di Caddy — Caddy otomatis menangani
Upgrade: websocketheader saat menggunakanreverse_proxy.- Status 101 (Switching Protocols) di access log menandakan WebSocket upgrade berhasil — gunakan ini untuk debugging.
- Untuk load balancing WebSocket, gunakan
lb_policy ip_hashagar client yang sama selalu diarahkan ke server yang sama (sticky session).- Jangan set
write_timeoutuntuk WebSocket — koneksi WebSocket bisa berlangsung berhari-hari dan timeout akan memutus koneksi aktif.- Autentikasi WebSocket dilakukan saat initial HTTP handshake — token bisa dikirim via query parameter atau cookie, bukan via header custom (tidak didukung setelah upgrade).
- Gunakan Redis sebagai shared state untuk arsitektur multi-server WebSocket yang lebih scalable dibanding sticky session.
← Sebelumnya: Python WSGI Berikutnya: SPA React →
WebSocket dengan Heartbeat dan Reconnect #
Koneksi WebSocket bisa terputus karena network issue atau idle timeout. Implementasikan heartbeat di client:
// Client-side WebSocket dengan auto-reconnect
class ReconnectingWebSocket {
constructor(url) {
this.url = url;
this.ws = null;
this.reconnectDelay = 1000;
this.heartbeatInterval = null;
this.connect();
}
connect() {
this.ws = new WebSocket(this.url);
this.ws.onopen = () => {
console.log('WebSocket connected');
this.reconnectDelay = 1000; // Reset delay
this.startHeartbeat();
};
this.ws.onclose = () => {
console.log(`Reconnecting in ${this.reconnectDelay}ms...`);
this.stopHeartbeat();
setTimeout(() => this.connect(), this.reconnectDelay);
// Exponential backoff (maks 30 detik)
this.reconnectDelay = Math.min(this.reconnectDelay * 2, 30000);
};
this.ws.onerror = (err) => console.error('WebSocket error:', err);
}
startHeartbeat() {
this.heartbeatInterval = setInterval(() => {
if (this.ws.readyState === WebSocket.OPEN) {
this.ws.send(JSON.stringify({ type: 'ping' }));
}
}, 25000); // Setiap 25 detik
}
stopHeartbeat() {
clearInterval(this.heartbeatInterval);
}
}
const ws = new ReconnectingWebSocket('wss://example.com/ws');
Implementasi heartbeat ini mencegah koneksi idle terputus oleh firewall atau proxy timeout, dan reconnect otomatis saat koneksi terputus karena network issue.