Sổ tay 40 bẫy
Tra nhanh theo triệu chứng. Mỗi bẫy: dấu hiệu nhận ra, nguyên nhân, cách chữa. Dùng khi đang kẹt, không phải để đọc một lượt.
Bài này để tra, không phải để đọc hết. Bấm Ctrl+F tìm theo triệu chứng bạn đang gặp.
Môi trường — máy Windows
1. Chữ tiếng Việt trong lệnh thành ? hoặc rụng dấu.
Dấu hiệu: trong cùng một câu, chữ một dấu rụng dấu (ư→u), chữ nhiều dấu thành ? (ự→?).
Chữa: ghi dữ liệu ra file rồi curl --data-binary "@file.json". Không nhét chuỗi có dấu vào dòng lệnh.
2. node/git/wrangler "không nhận lệnh" dù vừa cài.
Chữa: đóng hẳn cửa sổ dòng lệnh rồi mở lại — biến môi trường chỉ nạp khi mở cửa sổ mới.
3. wrangler treo, không trả về gì (Windows 11).
Nguyên nhân: Smart App Control chặn, hoặc Node 24 không spawn được file .cmd.
Chữa: gọi thẳng file .js: node <đường-dẫn>\wrangler\bin\wrangler.js --version.
4. Git báo cả nghìn dòng đổi dù chỉ sửa một chữ.
Nguyên nhân: kết thúc dòng CRLF/LF.
Chữa: git config --global core.autocrlf false.
5. Lệnh lỗi khó hiểu, đường dẫn có dấu hoặc dấu cách.
Chữa: để project ở đường dẫn không dấu, không dấu cách — C:\NVAI\Claude\ten-app.
6. Heredoc trong shell nuốt dấu gạch chéo ngược.
Dấu hiệu: biểu thức \d trong script thành d.
Chữa: viết script bằng trình soạn thảo, đừng truyền qua heredoc.
7. Thay chuỗi trong mã: $$ biến thành $.
Nguyên nhân: hàm thay chuỗi coi $$ là ký tự đặc biệt.
Chữa: dùng s.split(from).join(to) thay cho s.replace(from, to).
HTML / giao diện
8. Chữ tiếng Việt thành á, ô vuông. → thiếu <meta charset="utf-8"> ở đầu <head>.
9. Trên điện thoại chữ bé xíu, phải phóng to. → thiếu <meta name="viewport" content="width=device-width,initial-scale=1">.
10. iOS tự phóng to khi chạm ô nhập. → cỡ chữ ô nhập dưới 16px. Đặt font-size:16px cho mọi input, select, textarea.
11. Trang cuộn ngang trên điện thoại. Tìm thủ phạm: dán vào Console —
[...document.querySelectorAll('*')].filter(e=>e.getBoundingClientRect().right>innerWidth+1).map(e=>e.tagName+'.'+e.className)
12. Nhãn nút bị bẻ dòng. Nhãn tiếng Việt dài hơn tiếng Anh ~30%. Đo đúng: đếm số hình chữ nhật của nút văn bản (getClientRects()), đừng so chiều cao phần tử — nút có chữ căn giữa luôn cao hơn chữ.
13. Trang trắng hoàn toàn, Console cũng trống. → HTML vỡ cấu trúc, thường thiếu một thẻ đóng. Bấm Ctrl+U xem mã nguồn, hoặc mở trong trình soạn thảo xem chỗ gạch đỏ.
14. Cannot read properties of null. → <script> chạy trước khi HTML dựng xong (đưa xuống cuối <body>), hoặc gõ sai id (phân biệt hoa thường).
15. Chữ người dùng nhập chạy được như mã. → đang dùng innerHTML. Đổi sang textContent.
Cloudflare Pages / Workers
16. Sửa rồi deploy rồi mà vẫn thấy bản cũ. → bộ nhớ đệm trình duyệt. Ctrl+Shift+R hoặc cửa sổ ẩn danh. Vẫn cũ thì kiểm bạn có đang mở link preview cũ không.
17. Đổi ảnh/CSS mà người dùng vẫn thấy bản cũ hàng tuần. → header immutable. Đổi nội dung thì phải đổi TÊN FILE, rồi sửa mọi tham chiếu.
18. Đặt header trong mã Worker mà không có tác dụng. → Static Assets phục vụ thẳng từ biên, mã Worker không chạy. Đặt trong public/_headers.
19. Một đường API bị "nuốt", không bao giờ chạy. → có file trong thư mục tài nguyên tĩnh trùng tên với đường đó.
20. Liên kết nội bộ bị chuyển hướng thừa. → Pages tự bỏ đuôi .html. Viết href="/lien-he" hoặc dùng thư mục /lien-he/.
21. env.DB thành undefined sau khi deploy bằng CI. → binding thêm tay trên dashboard bị wrangler.toml ghi đè. Mọi binding phải nằm trong wrangler.toml.
22. wallTime trong log rất lớn, tưởng app chậm. → wallTime cộng cả ctx.waitUntil. Đo từ phía người dùng: curl -w "%{time_total}".
23. Tính năng chạy ở bản thật nhưng không chạy ở bản preview. → ba thứ không chạy trên preview: binding rate limit, migration Durable Object, Cron Trigger.
24. Không gắn được tên miền vào Pages. → tên miền/subdomain đó đang thuộc một Worker khác. Một địa chỉ chỉ thuộc một nơi.
25. Biến đếm toàn cục cho số liệu vô nghĩa. → Cloudflare chạy nhiều bản mã song song, mỗi bản có biến riêng. Lưu vào D1/KV/Durable Object.
D1 / dữ liệu
26. Màn hình danh sách chậm kinh khủng. → D1 chậm theo byte trả về (~100 KB/giây), không theo số dòng. Trả cột gọn, có LIMIT, đưa ảnh sang R2.
27. App treo mà tỉ lệ lỗi bình thường. → nghẽn D1. Thường do việc nặng trong ctx.waitUntil, hoặc một app khác dùng chung cơ sở dữ liệu đang chạy truy vấn nặng.
28. Báo cáo "hôm nay" thiếu cả buổi tối. → múi giờ. D1 lưu UTC, Việt Nam là UTC+7 → mọi việc sau 17h bị tính sang hôm sau. Dùng date(tao_luc, '+7 hours').
29. Tiền lệch vài đồng mỗi tháng. → lưu tiền bằng số thực. Lưu INTEGER đơn vị đồng.
30. Bấm đúp nút Lưu ra hai bản ghi. → kiểm trùng trong mã là không đủ (hai request song song cùng thấy "chưa có"). Phải có ràng buộc UNIQUE ở cơ sở dữ liệu.
31. Người khác sửa đè mất dữ liệu mình vừa nhập. → giao diện gửi lên bản ghi thiếu trường. Máy chủ phải từ chối bản rút gọn.
32. Chữ tiếng Việt trong lệnh wrangler d1 execute bị hỏng. → như bẫy 1. Dùng --file=cau-lenh.sql.
Bảo mật
33. Nhân viên xem được dữ liệu người khác bằng cách đổi số trong đường dẫn. → thiếu ràng buộc theo người sở hữu trong truy vấn. Nút bị ẩn không phải là phân quyền.
34. 429 hàng loạt, người thật không vào được. → đếm trần theo IP. Mạng 4G Việt Nam dùng chung IP. Đếm theo ip+tên đăng nhập ở cửa đăng nhập, theo phiên cho phần còn lại.
35. Trần rate limit lúc chặn lúc không. → binding rate limit của Cloudflare đếm xấp xỉ theo từng điểm biên. Chỗ cần trần thật thì đếm bằng Durable Object.
36. Nhân viên nghỉ việc vẫn đăng nhập được. → khóa tài khoản nhưng không thu hồi phiên. Phải xóa phiên đang hoạt động.
37. Người ngoài dò được số điện thoại nào có trong hệ thống. → thông báo "sai tài khoản" khác "sai mật khẩu". Phải giống hệt nhau.
Việc nền / AI
38. Cron chạy sai giờ. → cron tính theo UTC. 8h sáng giờ Việt Nam = 0 1 * * *. Ngày trong tuần cũng lệch.
39. Việc nền ngừng chạy mà không ai biết. → không ai ngồi nhìn cron. Ghi nhật ký mỗi lượt, hiện "lần chạy cuối" lên trang quản trị, và báo động khi im lặng quá 2 chu kỳ.
40. AI bịa ra câu trả lời nghe rất hợp lý. → thiếu câu "chỉ dựa trên dữ liệu được cung cấp; không có thì nói không có" trong hướng dẫn hệ thống. Phép thử bắt buộc: hỏi một câu mà dữ liệu không có đáp án.
Khi không biết bắt đầu từ đâu, ba triệu chứng này hay gặp nhất:
"App chậm" → mở F12 → Network → nhìn cột Size. Trong 9/10 lần, con số đó giải thích hết. Màn hình 20 dòng nên khoảng 5–15 KB; nếu là vài MB thì đó là nguyên nhân.
"App treo" → kiểm theo thứ tự: (1) có việc nặng trong waitUntil không, (2) có app khác dùng chung D1 đang chạy nặng không, (3) có fetch ra ngoài nào thiếu hạn giờ không.
"Lúc được lúc không" → gần như luôn là một trong ba: bộ nhớ đệm (trình duyệt hoặc CDN), nhiều bản mã chạy song song với trạng thái riêng, hoặc trần rate limit đếm xấp xỉ.
Rà project này theo danh sách bẫy dưới đây. Với mỗi bẫy tìm thấy:
ghi file, dòng, và cách sửa nhỏ nhất. Bẫy nào không có thì bỏ qua,
đừng nhắc.
- SQL nối chuỗi thay vì .bind()
- Truy vấn danh sách thiếu LIMIT hoặc thiếu trần số dòng
- SELECT * cho màn hình danh sách
- Việc nhiều lượt gọi cơ sở dữ liệu trong ctx.waitUntil
- Biến tầng module dùng để đếm hoặc điều tiết
- fetch ra ngoài thiếu timeout
- Trần rate limit đếm theo IP
- innerHTML với dữ liệu người dùng
- Đường API thiếu kiểm quyền phía máy chủ
- Truy vấn thiếu ràng buộc theo người sở hữu dữ liệu
- Tiền lưu bằng số thực
- Thời gian xử lý không nhất quán về múi giờ
- Ảnh hoặc file lớn lưu trong cơ sở dữ liệu
- Thao tác ghi không có khóa chống trùng
- Khóa bí mật nằm trong mã
Chỉ đọc, không sửa. Sắp theo mức thiệt hại, nặng nhất trước.
Chạy đề này mỗi tháng một lần, và mỗi lần trước khi giao app cho khách.
- Nguồn: cả 40 bẫy đều từ sự cố hoặc lỗi thật, không có bẫy nào chép từ tài liệu.
- Cách dùng đúng: tra theo triệu chứng khi đang kẹt; và chạy đề tự rà định kỳ.
- Rủi ro còn lại: danh sách này bắt lỗi đã biết. Bẫy mới sẽ xuất hiện khi nền tảng đổi — nên mỗi lần bạn mất hơn 30 phút cho một lỗi, hãy thêm nó vào danh sách của chính mình. → Biến kinh nghiệm thành luật