Network Programming · Part 10 — Robust Networking: Errors, Retries & Debugging
Capstone: treat the network as unreliable — socket error codes, mandatory error handlers, timeouts at every layer, retries with backoff & jitter, reconnecting clients, and a layered debugging toolkit with Node.js + TypeScript.
This is Part 10 of 10 — the capstone of our network programming series with Node.js + TypeScript {Đây là Phần 10 / 10 — bài kết của series lập trình mạng với Node.js + TypeScript}. Parts 1–9 taught you how data travels, how to open sockets, speak HTTP, encrypt with TLS, frame messages, and scale servers {Phần 1–9 dạy dữ liệu đi thế nào, mở socket, nói HTTP, mã hóa TLS, đóng khung message, và scale server}. Today we answer the question every production system eventually faces: what happens when the network fails? {Hôm nay ta trả lời câu hỏi mọi hệ thống production cuối cùng đều gặp: điều gì xảy ra khi mạng hỏng?}.
The honest answer: failure is the default {Câu trả lời thật: lỗi là mặc định}. Packets drop, cables flap, DNS caches go stale, load balancers drain nodes, TLS certs expire, and clients disconnect mid-request {Packet rơi, cáp giật, cache DNS cũ, load balancer drain node, cert TLS hết hạn, client ngắt giữa request}. Code that assumes “if I called connect(), it worked forever” will crash, hang, or corrupt data {Code giả định “gọi connect() là xong mãi mãi” sẽ crash, treo, hoặc hỏng dữ liệu}. Robust networking means expecting failure and building recovery paths at every layer {Mạng robust nghĩa là chờ lỗi và xây đường phục hồi ở mọi tầng}.
The network is unreliable by default {Mạng mặc định không tin cậy}
In Part 1 we learned that TCP reassembles packets into an ordered stream {Ở Phần 1 ta học TCP ráp packet thành luồng có thứ tự}. That reliability is best-effort end-to-end — not a guarantee that your app will always succeed on the first try {Độ tin cậy đó là best-effort đầu-cuối — không đảm bảo app luôn thành công lần đầu}. Between your socket.write() and the peer’s socket.on('data') sit routers, NAT boxes, firewalls, kernel buffers, and GC pauses {Giữa socket.write() và socket.on('data') của peer có router, NAT, firewall, buffer kernel, và GC pause}.
Your process → OS socket buffer → NIC → Internet → … → peer
↑ any hop can drop, delay, reset, or refuse
Design assumption {Giả định thiết kế}: every outbound call can fail; every inbound connection can die without warning; every retry can make things worse if you are careless {mọi lời gọi ra có thể fail; mọi kết nối vào có thể chết không báo; mọi retry có thể làm tệ hơn nếu bạn cẩu thả}. The rest of this post is the toolkit to live with that reality {Phần còn lại của bài là bộ công cụ để sống với thực tế đó}.
Common socket errors {Lỗi socket phổ biến}
Node surfaces OS-level errors as err.code on Error objects (and as errno on some APIs) {Node đưa lỗi cấp OS ra err.code trên object Error (và errno trên một số API)}. Learn these by heart — they tell you which layer broke and what to do next {Học thuộc — chúng cho biết tầng nào hỏng và làm gì tiếp}.
| Code | What it means {Ý nghĩa} | Typical cause {Nguyên nhân} | What to do {Xử lý} |
|---|---|---|---|
ECONNREFUSED | Nothing is listening on that IP:port {Không ai listen IP:port đó} | Server down, wrong port, firewall DROP {Server tắt, sai port, firewall DROP} | Fix target, health-check upstream, retry with backoff {Sửa đích, health-check upstream, retry backoff} |
ECONNRESET | Peer closed the TCP connection abruptly {Peer đóng TCP đột ngột} | Server crash, idle timeout, proxy kill {Server crash, timeout idle, proxy kill} | Reconnect; do not assume partial writes succeeded {Kết nối lại; đừng giả định write một phần thành công} |
ETIMEDOUT | Operation exceeded the OS/network timeout {Vượt timeout OS/mạng} | Host unreachable, routing black hole, overloaded server {Host không tới, routing đen, server quá tải} | Shorter connect timeout + retry; check routing/DNS {Timeout connect ngắn hơn + retry; kiểm tra routing/DNS} |
EHOSTUNREACH | No route to the host {Không có route tới host} | Wrong IP, VPN down, local network issue {Sai IP, VPN tắt, lỗi mạng local} | Verify IP/DNS; not fixed by app retry alone {Xác minh IP/DNS; retry app không đủ} |
EPIPE | Write to a socket whose peer already closed {Ghi vào socket peer đã đóng} | You kept writing after end() / disconnect {Tiếp tục ghi sau end() / disconnect} | Attach error handler; stop writing on close {Gắn handler error; dừng ghi khi close} |
EADDRINUSE | Port already bound by another process {Port đã bị process khác bind} | Two servers on same port {Hai server cùng port} | Pick another port or stop the conflicting process {Chọn port khác hoặc dừng process xung đột} |
ENOTFOUND | DNS lookup failed — hostname has no address {DNS lookup fail — hostname không có địa chỉ} | Typo, expired domain, resolver outage (Part 4) {Gõ sai, domain hết hạn, resolver lỗi (Phần 4)} | Validate hostname; cache DNS carefully; retry resolver {Kiểm tra hostname; cache DNS cẩn thận; retry resolver} |
Key idea {Ý chính}:
ECONNREFUSEDis “nobody home”;ECONNRESETis “they hung up on you”;ETIMEDOUTis “I waited and gave up” {ECONNREFUSEDlà “không ai ở nhà”;ECONNRESETlà “bên kia cúp máy”;ETIMEDOUTlà “chờ quá lâu rồi bỏ cuộc”}.
Always attach an error handler {Luôn gắn handler error}
In Node.js, an error event on a socket with no listener is fatal — it throws and can crash your entire process {Trong Node.js, error event trên socket không có listener là fatal — ném exception và có thể crash cả process}. This catches beginners constantly {Lỗi này bẫy người mới liên tục}.
import { createServer, type Socket } from 'node:net';
function attachSafeHandlers(socket: Socket, label: string): void {
socket.on('error', (err: NodeJS.ErrnoException) => {
// Log with context — never let this event go unhandled.
console.error(`[${label}] socket error:`, err.code ?? err.message);
});
socket.on('close', (hadError) => {
console.log(`[${label}] closed`, hadError ? '(with error)' : '(clean)');
});
}
const server = createServer((socket) => {
attachSafeHandlers(socket, 'server-conn');
socket.write('hello\n');
});
server.on('error', (err: NodeJS.ErrnoException) => {
// Server-level errors too — e.g. EADDRINUSE on listen().
console.error('server error:', err.code ?? err.message);
});
server.listen(3000);
The same rule applies to tls.TLSSocket, http.Server, WebSocket, and child_process streams {Quy tắc tương tự cho tls.TLSSocket, http.Server, WebSocket, và stream child_process}: if it extends EventEmitter and emits error, you must listen {nếu kế thừa EventEmitter và emit error, bạn phải listen}. In async code, also handle rejected promises from fetch, connect(), and your own wrappers {Trong code async, cũng xử lý promise reject từ fetch, connect(), và wrapper của bạn}.
Timeouts at every layer {Timeout ở mọi tầng}
“No timeout” means “hang forever” {Không timeout nghĩa là “treo mãi mãi”}. Production code needs three different timeouts {Code production cần ba loại timeout khác nhau}:
1. Connect timeout {Timeout kết nối}
How long you wait for TCP (and TLS) handshake to finish {Chờ bao lâu cho bắt tay TCP (và TLS) hoàn tất}. node:net has no built-in connect timeout — you implement it {node:net không có connect timeout sẵn — bạn tự implement}:
import { Socket } from 'node:net';
export function connectWithTimeout(
host: string,
port: number,
connectMs: number,
): Promise<Socket> {
return new Promise((resolve, reject) => {
const socket = new Socket();
const timer = setTimeout(() => {
socket.destroy();
reject(new Error(`connect timeout after ${connectMs}ms`));
}, connectMs);
socket.once('connect', () => {
clearTimeout(timer);
resolve(socket);
});
socket.once('error', (err) => {
clearTimeout(timer);
reject(err);
});
socket.connect(port, host);
});
}
// Usage
const socket = await connectWithTimeout('127.0.0.1', 3000, 3_000);
2. Idle / socket timeout {Timeout idle / socket}
How long the connection can sit without receiving data before you assume it is dead {Kết nối có thể không nhận dữ liệu bao lâu trước khi coi là chết}. Essential for long-lived WebSocket and TCP clients (Part 6) {Cần thiết cho WebSocket và TCP client sống lâu (Phần 6)}:
socket.setTimeout(30_000); // 30 s idle
socket.on('timeout', () => {
console.warn('socket idle timeout — destroying');
socket.destroy();
});
Combine with application heartbeats (ping/pong frames) so idle timeout only fires on truly dead peers {Kết hợp heartbeat ứng dụng (ping/pong) để idle timeout chỉ kích hoạt khi peer thật sự chết}.
3. Overall request timeout {Timeout toàn bộ request}
A ceiling on the entire operation — connect + TLS + send + wait for response {Trần cho toàn bộ thao tác — connect + TLS + gửi + chờ response}. Node 18+ ships AbortSignal.timeout() {Node 18+ có sẵn AbortSignal.timeout()}:
async function fetchWithDeadline(url: string, totalMs: number): Promise<Response> {
const response = await fetch(url, {
signal: AbortSignal.timeout(totalMs),
});
return response;
}
try {
const res = await fetchWithDeadline('https://api.example.com/health', 5_000);
console.log('status:', res.status);
} catch (err) {
// DOMException with name 'TimeoutError' when AbortSignal fires.
console.error('request failed or timed out:', err);
}
For raw sockets, wrap the whole flow in Promise.race against a timer, or use AbortController in your own client library {Với socket thô, bọc toàn flow trong Promise.race với timer, hoặc dùng AbortController trong client library của bạn}. Rule of thumb {Quy tắc ngón tay cái}: connect timeout ≪ idle timeout ≪ overall request timeout {connect timeout ≪ idle timeout ≪ overall request timeout}.
Retries done right {Retry đúng cách}
Blind retries are how a brief outage becomes a multi-hour incident {Retry mù quáng biến sự cố ngắn thành sự cố nhiều giờ}. Three rules:
Only retry idempotent operations {Chỉ retry thao tác idempotent}
An operation is idempotent if doing it twice has the same effect as once {Idempotent nghĩa là làm hai lần có cùng hiệu ứng với một lần}.
| Safe to retry {An toàn retry} | Dangerous to retry {Nguy hiểm retry} |
|---|---|
GET, HEAD, PUT with full body | POST payment, POST create order |
| DNS lookup | Non-idempotent RPC without dedup key |
| TCP/WebSocket reconnect (new session) | Retry after partial write without framing (Part 8) |
If the server might have already processed your request, use an idempotency key header before retrying {Nếu server có thể đã xử lý request, dùng header idempotency key trước khi retry}.
Exponential backoff with jitter {Backoff lũy thừa có jitter}
When a service is down, every client retrying at the same interval creates a retry storm that keeps it down {Khi service down, mọi client retry cùng nhịp tạo bão retry khiến nó tiếp tục down}. Exponential backoff spaces attempts apart; jitter randomizes the delay so clients do not synchronize {Backoff lũy thừa giãn các lần thử; jitter random hóa delay để client không đồng bộ}.
export interface RetryOptions {
maxAttempts: number;
baseDelayMs: number;
maxDelayMs: number;
/** Return false to stop retrying this error immediately. */
shouldRetry?: (err: unknown, attempt: number) => boolean;
}
function sleep(ms: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms));
}
function backoffDelay(attempt: number, base: number, max: number): number {
const exp = Math.min(max, base * 2 ** (attempt - 1));
const jitter = Math.random() * exp * 0.3; // up to 30% random spread
return Math.floor(exp + jitter);
}
export async function retryWithBackoff<T>(
fn: () => Promise<T>,
opts: RetryOptions,
): Promise<T> {
const { maxAttempts, baseDelayMs, maxDelayMs, shouldRetry } = opts;
let lastErr: unknown;
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
try {
return await fn();
} catch (err) {
lastErr = err;
if (shouldRetry && !shouldRetry(err, attempt)) throw err;
if (attempt === maxAttempts) break;
const delay = backoffDelay(attempt, baseDelayMs, maxDelayMs);
console.warn(`attempt ${attempt} failed, retrying in ${delay}ms`, err);
await sleep(delay);
}
}
throw lastErr;
}
// Example: retry GET on transient network errors only
async function fetchHealth(): Promise<string> {
return retryWithBackoff(
async () => {
const res = await fetch('http://127.0.0.1:3000/health', {
signal: AbortSignal.timeout(2_000),
});
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return res.text();
},
{
maxAttempts: 5,
baseDelayMs: 200,
maxDelayMs: 8_000,
shouldRetry: (err) => {
const code = (err as NodeJS.ErrnoException).code;
return code === 'ECONNREFUSED' || code === 'ETIMEDOUT' || code === 'ECONNRESET';
},
},
);
}
Circuit breaker (one paragraph) {Circuit breaker (một đoạn)}
A circuit breaker watches failure rate and stops calling a sick dependency for a cooldown period {Circuit breaker theo dõi tỷ lệ lỗi và ngừng gọi dependency đang bệnh trong thời gian cooldown}. After N consecutive failures, the breaker opens — your code fails fast locally instead of hammering a dying server {Sau N lỗi liên tiếp, breaker mở — code fail nhanh tại chỗ thay vì đập server đang chết}. After a timeout it half-opens: one probe request goes through; success closes the circuit (normal retries resume), failure re-opens it {Sau timeout breaker nửa-mở: một request thăm dò; thành công đóng mạch (retry bình thường), thất bại mở lại}. You do not need a heavy library on day one — a counter, a disabledUntil timestamp, and the backoff helper above are enough for most Node services {Không cần thư viện nặng ngay — counter, timestamp disabledUntil, và helper backoff ở trên đủ cho hầu hết service Node}.
A resilient reconnecting client {Client reconnect có khả năng phục hồi}
Long-lived connections — WebSocket chat (Part 6), TCP telemetry, game sessions — will drop {Kết nối sống lâu — chat WebSocket (Phần 6), telemetry TCP, session game — sẽ rớt}. Pattern: on close or error, schedule reconnect with capped exponential backoff, reset backoff on successful open {Pattern: khi close hoặc error, lên lịch reconnect với backoff lũy thừa có trần, reset backoff khi open thành công}.
import WebSocket from 'ws';
interface ReconnectOptions {
url: string;
minDelayMs: number;
maxDelayMs: number;
}
export function createReconnectingWebSocket(opts: ReconnectOptions): WebSocket {
let attempt = 0;
let ws: WebSocket;
let reconnectTimer: ReturnType<typeof setTimeout> | undefined;
const scheduleReconnect = (): void => {
attempt++;
const exp = Math.min(opts.maxDelayMs, opts.minDelayMs * 2 ** (attempt - 1));
const jitter = Math.random() * exp * 0.2;
const delay = Math.floor(exp + jitter);
console.warn(`reconnecting in ${delay}ms (attempt ${attempt})`);
reconnectTimer = setTimeout(connect, delay);
};
const connect = (): void => {
ws = new WebSocket(opts.url);
ws.on('open', () => {
attempt = 0; // success — reset backoff
console.log('connected');
});
ws.on('message', (data) => {
console.log('←', data.toString());
});
ws.on('error', (err) => {
// MUST exist — prevents process crash.
console.error('ws error:', err.message);
});
ws.on('close', () => {
console.warn('ws closed — scheduling reconnect');
scheduleReconnect();
});
};
connect();
return ws!;
}
The same structure works for raw TCP with net.createConnection() {Cấu trúc tương tự cho TCP thô với net.createConnection()}: track attempt, cap delay, reset on connect, never skip error handler {theo dõi attempt, giới hạn delay, reset khi connect, không bỏ handler error}. On reconnect, re-authenticate and replay state (subscriptions, cursor offsets) — the wire is new even if the URL is the same {Khi reconnect, xác thực lại và replay state (subscription, cursor) — đường truyền mới dù URL giống}.
The debugging toolkit {Bộ công cụ debug}
When something breaks, pick the tool at the layer where the symptom lives {Khi hỏng, chọn công cụ ở tầng có triệu chứng}. Do not reach for Wireshark when the bug is a malformed HTTP header {Đừng dùng Wireshark khi lỗi là HTTP header sai}.
Application layer — HTTP & APIs {Tầng ứng dụng — HTTP & API}
# Verbose HTTP — see request, response headers, TLS timing, redirects
curl -v http://127.0.0.1:3000/health
curl -v https://api.example.com/users -H 'Accept: application/json'
# Time breakdown: DNS, connect, TLS, TTFB
curl -w 'dns:%{time_namelookup} connect:%{time_connect} tls:%{time_appconnect} ttfb:%{time_starttransfer}\n' -o /dev/null -s https://example.com
curl -v is the fastest way to answer “is the server returning what I think?” without writing client code {curl -v là cách nhanh nhất trả lời “server có trả đúng như tôi nghĩ?” mà không viết client code}.
Socket & TLS layer — raw bytes {Tầng socket & TLS — byte thô}
# Raw TCP — type bytes, see what the server sends (Part 2)
nc -v 127.0.0.1 3000
echo 'PING' | nc -w 3 127.0.0.1 9000
# TLS handshake + cert chain (Part 7)
openssl s_client -connect example.com:443 -servername example.com </dev/null
openssl s_client shows the certificate chain, cipher, and whether SNI matches {openssl s_client hiện chuỗi certificate, cipher, và SNI có khớp}. Use it when fetch fails with certificate errors but you are unsure why {Dùng khi fetch fail lỗi certificate mà bạn không chắc vì sao}.
Reachability & socket table {Khả năng tới & bảng socket}
# Is the host alive at the IP layer?
ping -c 3 8.8.8.8
traceroute api.example.com
# What is listening? Established connections? (Linux: ss; macOS: netstat)
ss -tlnp
ss -tn state established
netstat -an | grep LISTEN
If ping fails but curl works, the problem is above IP (or ICMP is blocked) {Nếu ping fail nhưng curl ok, vấn đề trên tầng IP (hoặc ICMP bị chặn)}. If nothing is LISTEN on your port, ECONNREFUSED is expected {Nếu không có gì LISTEN trên port, ECONNREFUSED là đúng}.
Packet capture — ground truth {Bắt packet — sự thật cuối cùng}
When application logs and curl disagree with reality, capture packets {Khi log ứng dụng và curl không khớp thực tế, bắt packet}:
# Capture TCP port 3000 on loopback (Linux; macOS: lo0)
sudo tcpdump -i any port 3000 -nn -A
# TLS on 443 — ciphertext on the wire; combine with openssl s_client for keys in dev
sudo tcpdump -i any host api.example.com and port 443 -nn
Reading one tcpdump line {Đọc một dòng tcpdump}:
12:34:56.789012 IP 192.168.1.10.54321 > 93.184.216.34.443: Flags [P.], seq 100, ack 50, win 256, length 42
192.168.1.10.54321→93.184.216.34.443— direction: your client port 54321 to server 443 {hướng: client port 54321 tới server 443}.Flags [P.]— Push (application data), . ack field valid {Push (dữ liệu ứng dụng), . trường ack hợp lệ}.seq 100/ack 50— TCP sequence numbers for reorder/retransmit debugging {số thứ tự TCP để debug reorder/retransmit}.length 42— 42 bytes of payload in this segment {42 byte payload trong segment này}.
Wireshark adds decoding for HTTP, TLS, DNS, and WebSocket — same capture, richer view {Wireshark decode HTTP, TLS, DNS, WebSocket — cùng capture, nhìn phong phú hơn}. In Part 7 we encrypted the wire; in captures you will see TLS Application Data records, not plaintext passwords {Ở Phần 7 ta mã hóa đường truyền; trong capture bạn thấy bản ghi TLS Application Data, không phải mật khẩu plaintext}.
Putting it together: a small resilient HTTP client {Gắn lại: HTTP client nhỏ có khả năng phục hồi}
import { retryWithBackoff } from './retry-with-backoff.js';
export async function resilientGet(path: string): Promise<string> {
return retryWithBackoff(
async () => {
const res = await fetch(`http://127.0.0.1:3000${path}`, {
signal: AbortSignal.timeout(4_000),
});
if (res.status >= 500) throw new Error(`server error ${res.status}`);
if (!res.ok) throw new Error(`client error ${res.status}`);
return res.text();
},
{
maxAttempts: 4,
baseDelayMs: 100,
maxDelayMs: 4_000,
shouldRetry: (err, attempt) => {
const code = (err as NodeJS.ErrnoException).code;
const msg = err instanceof Error ? err.message : '';
if (msg.includes('server error')) return true;
return code === 'ECONNREFUSED' || code === 'ETIMEDOUT';
},
},
);
}
Every layer covered: overall timeout on fetch, retry only on transient / 5xx, backoff with jitter inside retryWithBackoff, and underlying sockets still need error handlers on servers you own {Mọi tầng: timeout tổng trên fetch, chỉ retry transient / 5xx, backoff + jitter trong retryWithBackoff, và socket bên dưới vẫn cần handler error trên server bạn sở hữu}.
Mistakes beginners make {Lỗi người mới hay mắc}
- ❌ No
errorhandler on sockets or servers — oneECONNRESETcrashes the Node process {Không handlererrortrên socket/server — mộtECONNRESETcrash process Node}. - ❌ Retrying non-idempotent requests — a duplicated
POST /chargecan double-bill a customer {Retry request không idempotent —POST /chargetrùng có thể tính tiền hai lần}. - ❌ Retry storms without backoff/jitter — clients synchronize and DDoS your own recovering service {Bão retry không backoff/jitter — client đồng bộ và DDoS service đang hồi phục}.
- ❌ No timeouts — a stuck
connect()orfetch()holds event-loop work and file descriptors forever {Không timeout —connect()hoặcfetch()kẹt giữ event-loop và file descriptor mãi}. - ❌ Swallowing errors silently — empty
catch {}blocks hideENOTFOUNDandECONNREFUSEDuntil users complain {Nuốt lỗi im lặng —catch {}rỗng cheENOTFOUNDvàECONNREFUSEDđến khi user phàn nàn}.
Exercises {Bài tập}
Try each before opening the solution {Thử từng bài trước khi mở lời giải}.
- Start a TCP server on port 3000, then connect with
nc. Kill the server whilencis connected — observeECONNRESETon the server if you omit anerrorhandler {Chạy server TCP port 3000, kết nốinc. Tắt server khincđang kết nối — quan sátECONNRESETtrên server nếu thiếu handlererror}. - Write a
retryWithBackoffcall that retriesfetch('http://127.0.0.1:9/')(port 9 is discard — nothing listens) and log each delay. Confirm delays grow and differ between runs (jitter) {ViếtretryWithBackoffgọifetch('http://127.0.0.1:9/')và log mỗi delay. Xác nhận delay tăng và khác giữa các lần chạy (jitter)}. - Run
curl -v http://127.0.0.1:3000/against your Part 5 HTTP server, then runsudo tcpdump -i lo0 port 3000 -nn -A(macOS) orsudo tcpdump -i any port 3000 -nn -A(Linux) in another terminal. Identify one[P.]line and label source, destination, andlength{Chạycurl -vvào server HTTP Phần 5, đồng thờitcpdumpở terminal khác. Tìm một dòng[P.]và ghi source, destination,length}.
Solution {Lời giải}
// Exercise 1 — server without error handler crashes on client reset
import { createServer } from 'node:net';
const server = createServer((socket) => {
// Missing socket.on('error') → process may throw on ECONNRESET
socket.on('data', (buf) => socket.write(buf));
});
server.listen(3000, () => console.log('listening :3000'));
// Terminal 2: nc localhost 3000
// Kill server (Ctrl+C) or kill nc abruptly → watch for uncaughtExceptionFix: add socket.on('error', () => {}) and server.on('error', () => {}) — the process stays alive {Sửa: thêm socket.on('error') và server.on('error') — process sống sót}.
// Exercise 2 — jittered backoff on ECONNREFUSED
import { retryWithBackoff } from './retry-with-backoff.js';
await retryWithBackoff(
async () => fetch('http://127.0.0.1:9/'),
{ maxAttempts: 4, baseDelayMs: 200, maxDelayMs: 3_000 },
);
// Logs show ~200ms, ~400ms, ~800ms ± jitter — different each run# Exercise 3
curl -v http://127.0.0.1:3000/
sudo tcpdump -i lo0 port 3000 -nn -A # macOS loopback
# Example line:
# IP 127.0.0.1.54321 > 127.0.0.1.3000: Flags [P.], seq 1, ack 1, win 6379, length 78
# Source: 127.0.0.1:54321 (curl client)
# Destination: 127.0.0.1:3000 (your server)
# length 78: 78 bytes of HTTP request payload in this segmentSeries recap {Tóm tắt series}
You now have the full path from bits on the wire to production-grade behavior {Bạn đã có lộ trình đầy đủ từ bit trên dây đến hành vi production}:
- Part 1 — Networking fundamentals — layers, IP, ports, packets
- Part 2 — TCP sockets — connect, listen, byte streams
- Part 3 — UDP datagrams — connectionless, lossy-by-design
- Part 4 — DNS & addressing — names to IPs,
ENOTFOUND - Part 5 — HTTP from the socket up — request/response framing
- Part 6 — WebSockets & real-time — upgrade, push, heartbeats
- Part 7 — TLS & HTTPS — encryption, certificates,
node:https - Part 8 — Streams, backpressure & framing — message boundaries over TCP
- Part 9 — Concurrency & scaling servers — workers, connection fan-out
Part 10 ties it together: expect failure, bound every wait with a timeout, retry only what is safe, and debug layer by layer {Phần 10 gắn kết: chờ lỗi, giới hạn mọi chờ bằng timeout, chỉ retry cái an toàn, và debug từng tầng}.
Takeaway {Điều cốt lõi}
The network is not a function that always returns — it is a probabilistic channel {Mạng không phải hàm luôn trả về — nó là kênh xác suất}. Attach error handlers, set connect / idle / request timeouts, retry idempotent work with backoff + jitter, reconnect long-lived clients with capped delay, and when logs are not enough, curl -v → nc / openssl s_client → ss / ping → tcpdump until the layer tells the truth {Gắn handler error, đặt timeout connect / idle / request, retry công việc idempotent với backoff + jitter, reconnect client sống lâu có trần delay, và khi log không đủ, curl -v → nc / openssl s_client → ss / ping → tcpdump đến khi tầng đó nói sự thật}. That mindset is what separates a demo server from software that survives Tuesday afternoon deploys {Tư duy đó tách server demo khỏi phần mềm sống sót qua đợt deploy chiều thứ Ba}.