Error UND_ERR_SOCKET pada Route Handler Next.js sering muncul secara tiba-tiba ketika aplikasi menerima beban lalu lintas tinggi dan layanan upstream mengalami penurunan performa. Gejala umum yang terlihat mencakup lonjakan tajam pada metrik latensi p99, penumpukan antrean koneksi, hingga kegagalan masif pada pemanggilan fetch internal dengan log error dari engine Undici.
Akar Masalah: Socket Exhaustion pada Pool Undici
Secara default, Next.js pada Node.js runtime mengandalkan implementasi fetch global yang ditenagai oleh Undici. Undici mengelola koneksi HTTP menggunakan connection pooling per-origin dengan batas koneksi simultan tertentu (secara default 100 soket aktif per host pada klien HTTP standar).
Ketika klien (browser atau API consumer) memutus koneksi di tengah jalan—misalnya karena pengguna menutup tab, menavigasi halaman lain, atau timeout lokal klien terpicu—Next.js tidak otomatis menghentikan proses eksekusi fetch ke upstream jika kita tidak menghubungkan siklus hidup request tersebut. Akibatnya:
- Downstream client sudah putus, tetapi eksekusi Route Handler tetap berjalan.
- Pemanggilan
fetchupstream tetap menahan satu slot soket di dalam pool Undici hingga socket timeout internal upstream tercapai. - Pada traffic tinggi dengan upstream yang lambat, ratusan soket menggantung secara bersamaan. Pool koneksi habis (socket exhaustion), dan request berikutnya langsung ditolak dengan pesan
UND_ERR_SOCKET.
Pola Kode Rentan (Sebelum Perbaikan)
Contoh berikut memperlihatkan implementasi umum Route Handler yang rentan terhadap kebocoran koneksi soket:
// app/api/data/route.ts
import { NextResponse } from 'next/server';
export const runtime = 'nodejs';
export async function GET() {
try {
// Masalah: Tidak ada sinyal pembatalan dari klien downstream
// dan tidak ada batas timeout upstream yang eksplisit.
const res = await fetch('https://api.internal.local/slow-resource');
if (!res.ok) {
return NextResponse.json({ error: 'Upstream failed' }, { status: 502 });
}
const data = await res.json();
return NextResponse.json(data);
} catch (err: unknown) {
// Semua error dicatat sebagai 500, termasuk pembatalan socket
console.error('Unhandled Route Error:', err);
return NextResponse.json({ error: 'Internal Server Error' }, { status: 500 });
}
}Strategi Mitigasi
1. Propagasi AbortSignal dari NextRequest
Objek NextRequest menyediakan properti request.signal berjenis AbortSignal. Sinyal ini otomatis memicu abort event saat klien downstream memutus koneksi. Mengoperkan request.signal ke opsi signal pada pemanggilan fetch memastikan soket upstream segera dilepaskan ke pool begitu downstream terputus.
2. Defensif Timeout via AbortSignal.any()
Mengandalkan request.signal saja belum cukup jika klien downstream tetap menunggu tetapi upstream membeku tanpa menutup koneksi. Diperlukan kombinasi antara request.signal klien dan batas waktu lokal menggunakan AbortSignal.any() dan AbortSignal.timeout().
3. Konfigurasi Undici Dispatcher untuk Batas Soket
Jika upstream membutuhkan kapasitas paralel lebih besar atau batas timeout yang lebih ketat, konfigurasikan custom Agent dari paket undici dan teruskan melalui opsi dispatcher pada fetch.
4. Isolasi Error Handling untuk Mencegah Log 500 Palsu
Ketika koneksi dibatalkan karena klien disconnect, Node.js akan melempar error dengan tipe AbortError. Error ini merupakan perilaku normal dan tidak boleh dicatat sebagai kegagalan server 500 yang merusak visibilitas alert pemantauan (APM).
Pola Kode Tangguh (Setelah Perbaikan)
Berikut implementasi Route Handler yang menerapkan propagasi sinyal, batas waktu defensif, custom connection pool, dan penanganan status pemutusan koneksi:
// app/api/data/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { Agent } from 'undici';
export const runtime = 'nodejs';
// Konfigurasi pool soket khusus untuk origin upstream
const upstreamDispatcher = new Agent({
connections: 200, // Tingkatkan batas soket bersamaan
connectTimeout: 5000, // Timeout inisiasi soket TCP (5 detik)
headersTimeout: 10000, // Timeout respons header (10 detik)
bodyTimeout: 15000, // Timeout transfer stream body (15 detik)
keepAliveTimeout: 30000, // Idle socket cleanup
});
const UPSTREAM_TIMEOUT_MS = 8000;
export async function GET(request: NextRequest) {
// Gabungkan sinyal downstream disconnect dan batas timeout lokal
const timeoutSignal = AbortSignal.timeout(UPSTREAM_TIMEOUT_MS);
const combinedSignal = AbortSignal.any([request.signal, timeoutSignal]);
try {
const res = await fetch('https://api.internal.local/slow-resource', {
signal: combinedSignal,
// @ts-expect-error - Undici dispatcher didukung pada fetch runtime Node.js
dispatcher: upstreamDispatcher,
headers: {
'Accept': 'application/json',
},
});
if (!res.ok) {
return NextResponse.json(
{ error: 'Upstream returned an error' },
{ status: res.status }
);
}
const data = await res.json();
return NextResponse.json(data);
} catch (err: unknown) {
if (err instanceof Error) {
// Kasus 1: Klien memutus koneksi (Tab ditutup, request dibatalkan)
if (request.signal.aborted) {
return new NextResponse(null, { status: 499 }); // 499: Client Closed Request
}
// Kasus 2: Batas waktu upstream tercapai
if (err.name === 'TimeoutError' || timeoutSignal.aborted) {
console.warn('[Upstream Timeout] API internal tidak merespons tepat waktu.');
return NextResponse.json(
{ error: 'Gateway Timeout' },
{ status: 504 }
);
}
// Kasus 3: Soket Undici putus di tengah transfer
if ('code' in err && err.code === 'UND_ERR_SOCKET') {
console.error('[Undici Socket Fault] Pool socket terputus:', err.message);
return NextResponse.json(
{ error: 'Upstream Connection Terminated' },
{ status: 502 }
);
}
}
console.error('[Unhandled Route Error]', err);
return NextResponse.json(
{ error: 'Internal Server Error' },
{ status: 500 }
);
}
}Analisis dan Dampak Operasional
Penerapan pola di atas memberikan mitigasi konkret terhadap stabilitas backend Next.js:
- Pelepasan Soket Cepat: Saat pengguna melakukan spam refresh atau membatalkan navigasi, pemanggilan upstream segera dibatalkan secara deterministik melalui event loop, sehingga soket Undici langsung kembali ke status idle atau dibersihkan tanpa menunggu TCP keep-alive hang.
- Ketahanan Sistem terhadap Degradasi Upstream: Penggunaan
AbortSignal.timeout()menjamin Route Handler tidak akan menunggu selamanya, mencegah akumulasi ribuan thread/soket pasif yang dapat memicu Node.js Out-Of-Memory (OOM). - Metrik Bersih: HTTP status 499 atau 504 membedakan kendala jaringan eksternal dari error internal aplikasi (500), mencegah pemicuan alarm peringatan palsu pada sistem monitoring backend.
Komentar
0 komentar
Masuk ke akun kamu untuk ikut berkomentar.
Belum ada komentar
Jadilah yang pertama ikut berdiskusi!