Hình dung một endpoint nhận dữ liệu như nhân viên bưu điện đứng quầy nhận gói. Ba việc anh ta phải làm, và cả ba đều có cái bẫy: phải mở bao ra mới biết bên trong có gì (không mở thì tay trắng); phải có tấm bảng "không nhận gói quá X ký" ở cửa (không thì ai đó đổ nguyên xe tải vào); và khi có người dúi cho một mớ giấy lộn dán sai địa chỉ, anh phải dán nhãn "hồ sơ không hợp lệ" trả lại người gửi — chứ không phải viết biên bản "bưu điện hỏng". Phần trước đo Router; bài này về ba việc đó, và Vert.x Web làm khác các khung quen thuộc ở một chỗ quan trọng.
Thân request không tự có
Vert.x đọc request theo luồng. Thân chỉ được gom lại nếu bạn bảo nó gom:
POST /khong-body -> body = null (không có BodyHandler)
POST /sau -> body = null (BodyHandler đăng ký SAU route)
POST /dung -> body = {"ten":"An"}
Dòng giữa là cái bẫy thật. Đăng ký BodyHandler sau route cần nó thì không có lỗi, không có cảnh báo — chỉ là body luôn null. Và vì phần trước đo được rằng route chạy theo đúng thứ tự đăng ký, nên đây là hệ quả trực tiếp chứ không phải một quy tắc riêng cần nhớ.
// dat o dau, truoc moi route can doc than
r.route().handler(BodyHandler.create());
Đặt nó ngay dòng đầu tiên sau khi tạo Router, trừ khi bạn có lý do rõ ràng để không gom thân của một số route — ví dụ endpoint nhận tệp lớn mà bạn muốn xử lý theo luồng.
Giới hạn kích thước
BodyHandler.create().setBodyLimit(1024);
gửi 501 byte -> [HTTP 200] nhận 501 byte
gửi 5 001 byte -> [HTTP 413]
Vert.x trả 413 Payload Too Large và cắt kết nối trước khi đọc hết — đó là điều bạn muốn, vì nó có nghĩa là một client gửi 10 GB không làm bộ nhớ máy chủ phình lên.
Một chi tiết đáng biết: trong failureHandler, c.failure() ở trường hợp này là null. Bạn chỉ có c.statusCode() bằng 413, không có ngoại lệ nào để đọc. Nên đừng viết c.failure().getMessage() mà không kiểm null — chính chỗ đó sẽ ném NullPointerException bên trong failureHandler.
Và nhớ đặt giới hạn này: mặc định là không giới hạn.
Chỗ lỗi của client bị báo thành lỗi của server
JsonObject j = c.body().asJsonObject();
if (j == null || !j.containsKey("ten")) { c.fail(400, ...); return; }
{"ten":"An"} -> [HTTP 200] chao An
{"khong-co-ten":1} -> [HTTP 400] thiếu trường 'ten'
khong-phai-json -> [HTTP 500] DecodeException: Failed to decode: Unrecognized token 'khong'...
asJsonObject() ném khi thân không phải JSON hợp lệ, và ngoại lệ đó đi thẳng vào failureHandler với mã 500 — tức là bạn đang báo cho client rằng máy chủ hỏng, trong khi chính họ gửi sai.
Hậu quả không chỉ là mã HTTP xấu: 500 làm nổi cảnh báo của đội vận hành, làm bẩn tỉ lệ lỗi, và che mất lỗi 500 thật. Một script gõ sai có thể làm biểu đồ sức khoẻ dịch vụ đỏ rực.
Cách chữa gọn nhất là bọc lại và tự quyết định mã:
JsonObject j;
try {
j = c.body().asJsonObject();
} catch (DecodeException e) {
c.fail(400, e); // loi cua client, tra ve 400
return;
}
if (j == null || !j.containsKey("ten")) { c.fail(400, new IllegalArgumentException("thieu 'ten'")); return; }
Hoặc dùng module vertx-web-validation để khai schema một lần thay vì kiểm tay từng trường — nhưng nguyên tắc không đổi: mọi thứ đến từ client đều phải giả định là sai, và sai của client là 4xx.
Ba việc phải làm cho mọi endpoint nhận dữ liệu
BodyHandler đặt ở đầu router, không phải sau route.
Luôn setBodyLimit. Mặc định không giới hạn nghĩa là một request đủ lớn có thể làm hỏng cả tiến trình.
Phân biệt 4xx với 5xx cho bằng được. JSON hỏng, thiếu trường, sai kiểu — tất cả là 400. Chỉ khi bạn hỏng mới là 500.
Kiểm chuyện này mất đúng một dòng — thử dúi rác vào endpoint xem nó trả gì:
# endpoint cua ban tra ve gi khi nhan rac
curl -s -o /dev/null -w "%{http_code}\n" -X POST -H 'Content-Type: application/json' \
-d 'khong-phai-json' http://localhost:8080/api/cua-ban
Ra 500 nghĩa là mọi client gõ sai đều đang được ghi nhận như một sự cố của bạn.
Mẫu số chung
Mọi thứ đi qua ranh giới từ ngoài vào đều là đầu vào không tin được — nó sẽ có ngày méo mó, sai định dạng, hoặc cố tình độc hại — nên phải kiểm ngay tại rìa và xếp cái sai về đúng phía: đầu vào méo là lỗi người gửi (4xx), không phải lỗi bạn (5xx). Việc dán nhầm nhãn không chỉ xấu mã trạng thái mà đầu độc chính tín hiệu sức khoẻ của bạn: một mớ 500 giả do client gõ sai sẽ gọi đội trực dậy lúc nửa đêm, thổi phồng tỉ lệ lỗi, và chôn mất cái 500 thật giữa đám nhiễu — đúng bài học "cảnh báo trên triệu chứng thật" nhìn từ phía sinh ra triệu chứng. Ngữ nghĩa mã HTTP có sẵn cả một bộ từ vựng cho việc này (4xx = bên gọi, 5xx = bên phục vụ); giữ cho hai loại đừng lẫn là điều kiện để mọi thứ đo lường phía sau còn nói thật.
Điều thứ hai: một mặc định "không giới hạn" là một khẩu súng đã lên đạn, và một cấu hình sai im lặng còn tệ hơn một cái nổ to. setBodyLimit mặc định vô hạn nghĩa là một request đủ lớn đủ sức làm sập tiến trình — cùng họ với hàng đợi không trần hay totalParts không chặn: bất cứ thứ gì người ngoài bơm to được đều phải có mức trần tường minh. Còn BodyHandler đặt sau route thì body luôn null mà không một dòng lỗi nào — cái bẫy "đặt rồi mà không có hiệu lực", nơi thứ tự của middleware quyết định tất cả và sai thứ tự thì hỏng trong câm lặng. Hai phản xạ rút ra: chặn trên đầu vào theo kích thước trước khi tin nội dung, và với thứ phụ thuộc thứ tự thì kiểm hiệu lực thật chứ đừng tin là "mình đã gọi".
Bài sau: Vert.x Web Client — gọi HTTP ra ngoài, và cái bẫy quên đặt phép chờ.