Tại sao Request báo 200 OK nhưng API vẫn trả về lỗi 500?
Nhiều lập trình viên khi mới làm quen với DevTools thường gặp một tình huống khá "hack não": Tab Network báo request trả về màu xanh kèm mã 200 OK, nhưng ứng dụng phía giao diện lại thông báo lỗi, và khi bấm vào tab Response kiểm tra thì JSON trả về lại mang giá trị "status": 500 kèm nội dung "Internal Server Error".
Hiện tượng một đằng báo 200 OK ở Header nhưng nội dung bên trong lại chứa lỗi 500 rất hay xảy ra trong thực tế phát triển phần mềm. Sự lệch pha này bắt nguồn từ đâu, và mã trạng thái ở Header khác gì so với thuộc tính status nằm trong JSON?
1. Sự khác biệt bản chất: Tầng giao thức vs Tầng dữ liệu
Để không bị nhầm lẫn khi debug, chúng ta cần phân biệt rõ hai tầng xử lý của một yêu cầu web:
[ Client / Browser ]
│
│ (1) Gửi HTTP Request
▼
[ Proxy / Service Worker / Web Server ] ────► Trả về HTTP Response Header (e.g., Status Code 200)
│
│ (2) Xử lý App Code (Backend)
▼
[ Application Logic / Framework ] ────► Trả về Response Body JSON (e.g., {"status": 500, "error": "..."})
- HTTP Status Code ở Header (Tầng giao thức): Đây là mã trạng thái chính thức của giao thức HTTP do Web Server, Proxy hoặc Service Worker tạo ra. Nhiệm vụ của nó là báo cho trình duyệt biết bản thân việc truyền tải yêu cầu qua mạng có thành công hay không.
- Status Code trong Response Body (Tầng dữ liệu application): Đây đơn thuần là một trường dữ liệu (field) dạng chuỗi hoặc số do ứng dụng backend (hoặc framework như Spring Boot, Express.js) tự đóng gói vào chuỗi JSON. Trình duyệt xem đây là nội dung văn bản bình thường chứ không coi nó là mã điều khiển giao thức network.
2. Lỗi 500 thực sự xảy ra khi nào? Có thật là "không có Response"?
Một hiểu lầm rất phổ biến của developer là: Lỗi 500 có nghĩa là máy chủ đã sập hoàn toàn nên sẽ ngắt kết nối và không gửi về bất kỳ dữ liệu nào.
Trong thực tế, điều này không đúng:
- Lỗi 500 không phải là sập nguồn máy chủ: Mã
500 Internal Server Errorxuất hiện khi Web Server vẫn đang chạy bình thường, vẫn nhận được request từ client, nhưng trong quá trình thực thi đoạn code nghiệp vụ ở phía dưới, một ngoại lệ (Exception) không được xử lý (unhandled exception) đã bùng nổ (ví dụ:NullPointerException, lỗi kết nối CSDL tạm thời, v.v.). - Máy chủ vẫn trả về Response Body đầy đủ: Sau khi phát sinh ngoại lệ, framework phía backend sẽ đứng ra bắt lỗi này và giữ đúng quy chuẩn giao thức: Gửi về cho client một HTTP Response hoàn chỉnh. Phần Header sẽ mang mã
500, đồng thời phần Body sẽ chứa thông tin chi tiết về sự cố (thường kèm theorequestIdhoặctraceIdđể tra cứu log). - Khi nào máy chủ mới hoàn toàn "im lặng"? Trình duyệt chỉ thực sự không nhận được Response nào khi máy chủ bị mất điện, tràn RAM chết tiến trình (crash process), rớt mạng hoàn toàn, hoặc bị chặn bởi firewall. Lúc này trình duyệt sẽ hiển thị các mã lỗi mạng của chính nó như
ERR_CONNECTION_REFUSEDhayERR_TIMED_OUT.
3. Tại sao Header báo 200 OK nhưng Body JSON lại mang lỗi 500?
Có hai kịch bản chính dẫn đến hiện tượng "trái ngược" này:
Kịch bản A: Sự can thiệp của Service Worker
Nếu ứng dụng có sử dụng Service Worker (thường gặp ở PWA hoặc các hệ thống có cơ chế cache phức tạp), Service Worker sẽ đứng ở giữa trình duyệt và mạng để chặn request. Nếu Service Worker xử lý thành công việc giao tiếp hoặc trả về dữ liệu từ bộ nhớ đệm, nó sẽ đóng dấu 200 OK (from service worker) ở tầng Header. Tuy nhiên, nếu nội dung JSON mà nó đã lưu đệm trước đó lại chứa thông tin phản hồi lỗi từ backend, bạn sẽ thấy sự chênh lệch này.
Kịch bản B: Thiết kế "Custom Response Format" chưa chuẩn của Backend
Một số hệ thống Backend được thiết kế theo trường phái luôn luôn trả về HTTP Header 200 OK cho mọi request, sau đó bọc kết quả thực sự (dù thành công hay thất bại) vào bên trong một cấu trúc JSON chung:
{
"code": 500,
"message": "Internal Server Error",
"data": null
}
Kiểu thiết kế này giúp phía Frontend dễ dàng đọc response mà không bị các thư viện HTTP Client (như Axios) tự động bắn vào nhánh catch khi gặp mã 4xx/5xx. Tuy nhiên, nhược điểm là nó đi ngược lại với nguyên tắc RESTful tiêu chuẩn và khiến việc theo dõi hệ thống qua các công cụ Network Monitoring (như Datadog, New Relic) trở nên khó khăn hơn.
4. Ai chịu trách nhiệm quyết định Status Code ở Header?
Mã trạng thái ở Header không tự nhiên sinh ra, nó được quyết định ở 3 cấp độ hạ tầng:
- Backend Developer: Quyết định trả về
200khi xử lý thành công,400khi dữ liệu đầu vào sai,401/403khi chưa xác thực/không có quyền, hoặc404khi không tìm thấy dữ liệu. - Framework / App Server: Khi code bị văng lỗi bất ngờ mà dev chưa viết hàm hứng (
try-catch), các khung làm việc như Spring Boot hay ExpressJS sẽ tự động gán mã500vào Header trước khi trả về cho client. - Web Server / Reverse Proxy: Các hạ tầng đứng trước app như Nginx, Cloudflare, hoặc API Gateway sẽ tự quyết định Status Code nếu sự cố xảy ra ngay tại tầng của chúng (ví dụ:
502 Bad Gatewaykhi không nối được tới backend, hoặc504 Gateway Timeoutkhi backend xử lý quá lâu).
Bảng giải thích thuật ngữ (Glossary)
| Thuật ngữ | Giải thích ngắn gọn |
|---|---|
| HTTP Status Code | Mã trạng thái tiêu chuẩn được máy chủ gửi lại qua giao thức HTTP để thông báo kết quả của yêu cầu (ví dụ: 200 thành công, 404 không tìm thấy, 500 lỗi hệ thống). |
| HTTP Response Header | Phần đầu của phản hồi HTTP, chứa các thông tin metadata điều khiển giao tiếp như mã trạng thái, loại dữ liệu, cấu hình bảo mật hay bộ nhớ đệm. |
| Response Body (Payload) | Phần nội dung dữ liệu thực sự mà máy chủ gửi về cho trình duyệt, thường trình bày dưới dạng JSON, HTML hoặc văn bản. |
| Service Worker | Một đoạn script chạy ngầm độc lập ở trình duyệt, đứng giữa ứng dụng web và mạng để xử lý lưu đệm (cache), gửi thông báo hoặc chạy chế độ ngoại tuyến (offline). |
| DevTools | Bộ công cụ tích hợp sẵn trên trình duyệt (như Chrome DevTools) giúp lập trình viên kiểm tra giao diện, kiểm tra mạng (Network), và debug code. |
| Unhandled Exception | Ngoại lệ/Lỗi phát sinh trong code backend nhưng không được lập trình viên bắt (try-catch) kịp thời, khiến chương trình bị gián đoạn xử lý logic. |
All rights reserved