nicetool.dev logo
#http#api#backend

Mã trạng thái HTTP cho API: Những cặp mã lập trình viên hay nhầm nhất

401 hay 403? 400 hay 422? 301, 302, 307 hay 308? Hướng dẫn thực tế chọn đúng mã trạng thái HTTP cho REST API, kèm header cần có và các anti-pattern nên tránh.

8 phút đọc

Phần lớn API chỉ dùng vài mã trạng thái, vậy mà khi review code vẫn lặp lại những câu hỏi cũ. Token hết hạn thì trả 401 hay 403? Lỗi validate là 400 hay 422? Vì sao redirect lại biến POST thành GET? Client, proxy, trình duyệt và công cụ tìm kiếm đều phản ứng tự động theo những con số này, nên chọn sai mã gây ra lỗi thật chứ không chỉ là chuyện phong cách.

Bài viết này theo RFC 9110 (HTTP Semantics, 2022), tài liệu thay thế nhóm RFC 7231 cũ, và tập trung vào những cặp mã hay bị nhầm nhất.

Năm nhóm mã trong một phút

Nhóm Ý nghĩa Ai cần xử lý
1xx Thông tin, yêu cầu đang tiếp tục Hiếm khi gặp trong code ứng dụng
2xx Thành công Không ai, mọi thứ đã chạy
3xx Chuyển hướng Client đi theo Location hoặc dùng cache
4xx Lỗi phía client Client phải sửa yêu cầu
5xx Lỗi phía server Server phải sửa; client có thể thử lại

Ranh giới 4xx/5xx là quan trọng nhất. 4xx nghĩa là "đừng gửi lại y nguyên yêu cầu này", 5xx nghĩa là "lát nữa có thể sẽ được". Thư viện retry, load balancer và cảnh báo giám sát đều dựa trên giả định đó. Trả 500 cho dữ liệu đầu vào sai sẽ gây retry vô ích và cảnh báo giả. Trả 400 khi database sập lại che mất một sự cố thật.

Mã thành công: 200, 201, 202, 204

  • 200 OK: phản hồi thành công thông dụng, có body.
  • 201 Created: đã tạo tài nguyên mới. Kèm header Location chứa URL của nó, và thường trả luôn đối tượng vừa tạo trong body.
  • 202 Accepted: yêu cầu đã được đưa vào hàng đợi nhưng chưa xử lý xong, ví dụ xuất video. Hãy trả về cách theo dõi tiến độ, chẳng hạn URL của job.
  • 204 No Content: thành công và không có body. Rất hợp với DELETE hoặc PUT không cần trả gì. Đừng gửi {} kèm 204, vì một số client sẽ lỗi khi cố đọc phần body lẽ ra không tồn tại.

401 và 403

Đây là cặp hay nhầm nhất, một phần vì 401 có tên "Unauthorized" trong khi thực chất nó nghĩa là chưa xác thực.

  • 401 Unauthorized: "Tôi không biết bạn là ai." Thông tin đăng nhập bị thiếu, hết hạn hoặc không hợp lệ. Đặc tả yêu cầu kèm header WWW-Authenticate để chỉ cho client cách xác thực, ví dụ WWW-Authenticate: Bearer. Client nhận 401 nên làm mới token hoặc yêu cầu người dùng đăng nhập lại.
  • 403 Forbidden: "Tôi biết bạn là ai, và bạn không được phép làm việc này." Đăng nhập lại cũng không giúp gì. Người dùng thiếu quyền, thiếu gói dịch vụ hoặc không sở hữu tài nguyên.

Phép thử đơn giản: dùng thông tin đăng nhập khác thì yêu cầu này có thành công không? Nếu đăng nhập hợp lệ là giải quyết được, trả 401. Nếu người dùng này không bao giờ làm được, trả 403.

Có một ngoại lệ hữu ích. Khi chỉ riêng việc cho biết tài nguyên tồn tại đã là lộ thông tin, hãy trả 404 thay vì 403. GitHub làm vậy với các repository private mà bạn không có quyền, nên người ngoài không thể dò ra tên repo private bằng cách đoán.

400 và 422

  • 400 Bad Request: không hiểu được yêu cầu, ví dụ JSON sai cú pháp, sai Content-Type hoặc thiếu header bắt buộc.
  • 422 Unprocessable Content: yêu cầu đúng cú pháp nhưng nội dung không qua được kiểm tra, ví dụ email thiếu "@", ngày kết thúc trước ngày bắt đầu, số lượng nhỏ hơn 0. RFC 9110 đã đổi tên mã này từ "Unprocessable Entity".

Nhiều API dùng 400 cho tất cả, như vậy vẫn chấp nhận được miễn là nhất quán. Tách riêng hai mã giúp ích cho client: 400 thường là lỗi trong code client, còn 422 thường là người dùng nhập sai và nên được hiển thị lỗi theo từng trường.

Hai mã liên quan: 415 Unsupported Media Type khi client gửi XML tới endpoint chỉ nhận JSON, và 409 Conflict khi yêu cầu hợp lệ nhưng xung đột với trạng thái hiện tại, ví dụ đăng ký email đã tồn tại, hoặc sửa bản ghi mà người khác vừa thay đổi.

404 và 410

  • 404 Not Found: ở đây không có gì, và server không nói là tạm thời hay vĩnh viễn.
  • 410 Gone: tài nguyên từng tồn tại và đã bị xóa có chủ đích, vĩnh viễn.

Với API, 404 gần như luôn ổn. Với website, 410 cho công cụ tìm kiếm biết rõ hơn là trang bị xóa có chủ đích. Trên thực tế Google xử lý hai mã này gần như giống nhau, nên hãy dùng 410 khi bạn chắc chắn việc xóa là vĩnh viễn, nhưng không cần bận tâm chuyển đổi các trang 404 cũ.

Dù làm gì, hãy tránh "soft 404": trang báo lỗi nhưng trả mã 200 OK. Công cụ tìm kiếm có thể lập chỉ mục hàng nghìn trang "Không tìm thấy", còn hệ thống giám sát sẽ không bao giờ phát hiện link hỏng.

Chuyển hướng: 301, 302, 303, 307, 308

Các mã chuyển hướng khác nhau ở hai điểm: có vĩnh viễn không, và client có được đổi POST thành GET không?

Mã Vĩnh viễn Giữ nguyên method
301 Moved Permanently Có Không đảm bảo (POST thường thành GET)
302 Found Không Không đảm bảo
303 See Other Không Luôn đổi thành GET
307 Temporary Redirect Không Có
308 Permanent Redirect Có Có

Trước đây trình duyệt đổi POST thành GET khi gặp 301 và 302, và đặc tả hiện nay cho phép điều đó để tương thích ngược. Đó là lý do 307 và 308 ra đời. Trong thực tế:

  • Chuyển website sang tên miền hoặc đường dẫn mới: 301 (hoặc 308). Trình duyệt và công cụ tìm kiếm cache chuyển hướng vĩnh viễn, nên hãy kiểm tra kỹ trước khi đưa lên production.
  • Chuyển hướng endpoint API nhận POST hoặc PUT: 307 hoặc 308, để body không bị mất.
  • Sau khi gửi form, đưa người dùng tới trang kết quả (mẫu Post/Redirect/Get): 303, giúp tránh gửi trùng khi người dùng bấm tải lại trang.

304 Not Modified

Khi client gửi If-None-Match kèm ETag (hoặc If-Modified-Since) và tài nguyên chưa thay đổi, server trả 304 không có body, và client dùng bản trong cache. Phần lớn framework và CDN tự xử lý việc này cho file tĩnh. Với API, thêm ETag cho các phản hồi lớn, ít thay đổi giúp tiết kiệm băng thông mà gần như không tốn thêm code.

429 và 503: cho client biết khi nào quay lại

  • 429 Too Many Requests: client vượt giới hạn tần suất (rate limit). Gửi kèm header Retry-After (số giây hoặc một mốc thời gian HTTP) để client tử tế biết chờ, thay vì dồn dập gửi tiếp.
  • 503 Service Unavailable: server quá tải hoặc đang bảo trì. Retry-After cũng dùng được ở đây. Khi bảo trì theo kế hoạch, hãy trả 503 thay vì một trang bảo trì mã 200, để công cụ tìm kiếm biết đây chỉ là tạm thời và không thay nội dung đã lập chỉ mục.

500, 502, 504: lỗi của ai?

  • 500 Internal Server Error: lỗi không lường trước trong code của bạn. Ghi log đủ ngữ cảnh để debug, nhưng tuyệt đối không đưa stack trace vào phản hồi.
  • 502 Bad Gateway: proxy hoặc gateway (Nginx, load balancer, API gateway) nhận được phản hồi không hợp lệ từ server phía sau, thường vì ứng dụng bị crash hoặc không lắng nghe cổng.
  • 504 Gateway Timeout: proxy chờ server phía sau quá lâu.

Khi debug, mã lỗi cho biết nên tìm ở đâu. 502 hoặc 504 thường nghĩa là vấn đề nằm giữa proxy và ứng dụng, không nằm trong hàm xử lý request.

Các anti-pattern

  1. Trả 200 kèm body báo lỗi, kiểu {"success": false, "error": "Not found"}. Cache sẽ lưu lại, hệ thống giám sát tính là thành công và thư viện client không ném lỗi.
  2. Trả 500 cho lỗi validate. Kỹ sư trực sự cố bị đánh thức chỉ vì một lỗi gõ nhầm trong form.
  3. Mỗi endpoint một định dạng lỗi khác nhau. Hãy dùng một cấu trúc duy nhất. RFC 9457 Problem Details (application/problem+json) là tiêu chuẩn có sẵn với các trường type, title, status, detail và có chỗ cho lỗi từng trường:
{
  "type": "https://example.com/errors/validation",
  "title": "Dữ liệu không hợp lệ",
  "status": 422,
  "errors": [{ "field": "email", "message": "phải chứa @" }]
}
  1. Tự chế mã như 499 hay 520 cho API của mình. Đó là các mã riêng của từng sản phẩm (Nginx và Cloudflare), client thông thường sẽ không hiểu.

Còn 418 I'm a teapot? Mã này bắt đầu từ một RFC đùa ngày Cá tháng Tư năm 1998. RFC 9110 hiện giữ chỗ con số này để không ai gán cho nó ý nghĩa thật. Dùng làm "trứng phục sinh" thì vui, nhưng đừng dùng cho lỗi trên production.

Bảng quyết định nhanh

  • Tạo mới thành công → 201 + Location
  • Không có gì để trả → 204
  • Chưa đăng nhập hoặc token hết hạn → 401 + WWW-Authenticate
  • Đã đăng nhập nhưng không có quyền → 403 (hoặc 404 để giấu sự tồn tại)
  • Sai cú pháp → 400; đúng cú pháp nhưng dữ liệu không hợp lệ → 422
  • Xung đột trạng thái hoặc trùng lặp → 409
  • Vượt giới hạn tần suất → 429 + Retry-After
  • Lỗi code của bạn → 500; server phía sau hỏng → 502 / 504; bảo trì → 503

Khi gặp một mã lạ, bảng tra cứu Mã trạng thái HTTP liệt kê đầy đủ các mã chuẩn kèm giải thích dễ hiểu, và bạn có thể tìm theo số hoặc từ khóa.

Tác giả

Nguyễn Thành Nam

Full-Stack Engineer · Backend & Platform

Kỹ sư full-stack với hơn 9 năm kinh nghiệm xây dựng API và nền tảng lưu lượng lớn bằng Go, NestJS, PHP (Laravel, Phalcon) và Node.js. Chuyên về thiết kế API GraphQL, hệ thống thời gian thực và tối ưu hiệu năng; xây dựng NICETOOL.dev cho cộng đồng lập trình viên.

Tự thử ngay

Bài viết liên quan