Nghĩ về chuỗi handler như một đường chạy tiếp sức: mỗi chân chỉ có đúng hai việc hợp lệ — trao gậy cho chân sau (next()), hoặc về đích (gửi response). Đứng lì giữa đường ôm gậy không làm gì thì cuộc đua không bao giờ kết thúc, và chẳng ai bấm giờ báo lỗi cả — đó chính là một request treo vì quên next(). Còn hai người cùng tưởng mình về đích thì thành một cú va chạm. Bài này đo cái giá của tầng Vert.x Web, và cả ba kiểu "chân chạy sai luật" làm request biến mất không dấu vết.
Tám bài đầu dùng requestHandler trần. Từ đây trở đi là Vert.x Web — lớp trên cùng lo định tuyến, thân request, phiên, xác thực. Bài này đo cái giá của lớp đó, và câu trả lời làm tôi bất ngờ.
Router không tốn gì đo được
Câu hỏi tự nhiên khi thêm một lớp trừu tượng: nó ăn bao nhiêu? Đo ở 500 kết nối đồng thời:
| Thông lượng | p50 | |
|---|---|---|
requestHandler trần, không Router |
89 548 req/s | 4,9 ms |
| Router, 1 handler | 98 683 req/s | 4,8 ms |
| Router, 5 handler rỗng + 1 | 99 108 req/s | 4,9 ms |
| Router, 20 handler rỗng + 1 | 100 088 req/s | 4,8 ms |
| Router, 50 handler rỗng + 1 | 99 042 req/s | 4,6 ms |
Năm mươi handler trong chuỗi cho cùng thông lượng với một handler. Router thậm chí đo ra nhỉnh hơn requestHandler trần — chênh lệch nằm trong nhiễu, nên cách đọc đúng là: Router miễn phí.
Quên next(): request biến mất
r.get("/quen").handler(c -> {
kiemTraQuyen(c); // xong việc rồi... quên gọi next() và cũng không end()
});
curl hết giờ sau 6 giây, nhận 0 byte
Không phải 500, không phải timeout của server, không có dòng log nào. Request treo cho tới khi client bỏ cuộc, và kết nối vẫn mở phía server.
Đây là lỗi phổ biến nhất với Vert.x Web, và nó đặc biệt hay xảy ra trong nhánh else hoặc trong khối catch — chỗ người ta viết vội. Luật: mọi nhánh của mọi handler phải kết thúc bằng đúng một trong hai việc: next() hoặc gửi response.
Thứ tự route: đăng ký trước thắng
r.get("/thu-tu/*").handler(c -> c.response().end("route-rong"));
r.get("/thu-tu/cu-the").handler(c -> c.response().end("route-cu-the"));
GET /thu-tu/cu-the -> route-rong
Route cụ thể không bao giờ chạy. Vert.x Web duyệt theo thứ tự đăng ký và dừng ở cái khớp đầu tiên gửi response. Khác với một số khung tự sắp xếp theo độ cụ thể, ở đây thứ tự trong mã là thứ tự ưu tiên.
Hệ quả: đặt route bắt-tất (/*, /api/*) ở cuối, hoặc dùng next() trong đó nếu nó chỉ làm việc phụ như ghi log.
Ném ngoại lệ thì có failureHandler
r.route().failureHandler(c -> c.response()
.setStatusCode(c.statusCode() > 0 ? c.statusCode() : 500)
.end("lỗi: " + c.failure().getMessage()));
GET /nem -> [HTTP 500] lỗi: nem trong handler
Ngoại lệ ném đồng bộ trong handler được Vert.x Web bắt lại và đẩy vào failureHandler. Đây là khác biệt đáng kể so với Future ở phần 6, nơi ngoại lệ ném trong onSuccess bị nuốt mất hoàn toàn.
Nhưng nhớ: nó chỉ bắt được ngoại lệ đồng bộ. Lỗi xảy ra trong một callback bất đồng bộ vẫn phải tự gọi c.fail(t).
next() sau khi đã end()
client nhận: [HTTP 200] ok
server ghi log: IllegalStateException: Response has already been written
Client thấy mọi thứ bình thường, server ghi một ngoại lệ. Đây là kiểu lỗi chỉ lộ ra khi ai đó đọc log — và nó thường là dấu hiệu hai handler cùng nghĩ mình chịu trách nhiệm trả lời.
Ba luật
Mỗi nhánh kết thúc bằng next() hoặc response. Không có lựa chọn thứ ba.
Route bắt-tất đặt cuối. Hoặc cho nó next().
Luôn có failureHandler ở cấp router. Không có nó, ngoại lệ đồng bộ thành response rỗng.
Cách khoanh vùng nhanh những handler đáng nghi — cái nào có thể treo request mà không để dấu vết:
# tim handler khong co next() lan khong co response
grep -rn -A3 "\.handler(" src/ | grep -B2 "}" | grep -v "next()\|\.end(\|\.fail(" | head
Không hoàn hảo, nhưng mỗi dòng nó chỉ ra là một chỗ request có thể treo mà không nói cho ai biết.
Mẫu số chung
Một chuỗi xử lý là một hợp đồng: mỗi mắt xích buộc phải làm đúng một trong hai việc — chuyển tiếp (next) hoặc kết thúc (response) — không được cả hai, cũng không được không cái nào. Quên cả hai thì request treo im lặng (không lỗi, không log, chỉ có client ngồi chờ đến hết giờ); làm cả hai thì va chạm (IllegalStateException mà client vẫn thấy 200). Điều làm nó nguy là cả hai vi phạm đều không kêu: một cái treo vô hình, một cái chỉ hiện trong log không ai đọc. Cùng cái hợp đồng ấy ở mọi chuỗi middleware/filter — next() của Express, chuỗi Servlet Filter, một pipeline xử lý theo bước — và ở một máy trạng thái nơi mọi trạng thái phải có đường chuyển tiếp, một Promise phải resolve hoặc reject đúng một lần. Bài học: khi bạn là một mắt trong chuỗi, nghĩa vụ của bạn là kết thúc dứt khoát — chuyển tiếp hoặc trả lời — và nhánh dễ quên nhất chính là else/catch, chỗ viết vội, đúng chỗ để lọt cái treo vô hình.
Điều thứ hai: hệ thống làm đúng cái bạn viết theo đúng thứ tự bạn viết — nó không đoán ý bạn. Route /* đặt trước route cụ thể thì cái cụ thể không bao giờ chạy, vì luật là "khớp đầu tiên thắng theo thứ tự đăng ký", không phải "cái cụ thể nhất thắng". Khác với vài framework tự xếp theo độ cụ thể, ở đây thứ tự trong mã chính là thứ tự ưu tiên — và một cái bắt-tất đặt nhầm chỗ lặng lẽ nuốt mọi thứ sau nó. Cùng luật "thứ tự là quy tắc" ở một firewall/ACL khớp dòng đầu tiên, một case không break rơi xuyên, một .gitignore mà dòng sau đè dòng trước, CSS khi độ đặc hiệu bằng nhau thì dòng viết sau thắng. Khi một hệ dùng thứ tự làm luật, đọc từ trên xuống đúng như máy đọc, và đặt cái tổng quát ở nơi nó không che mất cái cụ thể — vì máy sẽ không sửa thứ tự giúp bạn theo điều bạn định.
Bài sau: BodyHandler — đọc thân request, giới hạn kích thước, và vì sao JSON hỏng lại trả về 500 thay vì 400.