Danh sách bài viết

Bài 3: HTTP Status Codes: 2xx/3xx/4xx/5xx

Bài 3 của series Rust RESTful API — tổng quan 5 class HTTP status code (1xx, 2xx, 3xx, 4xx, 5xx) theo RFC 9110, top 20 code phổ biến trong REST API như 200, 201, 204, 301, 304, 400, 401, 403, 404, 409, 422, 429, 500, 502, 503, 504, cách chọn đúng code cho mỗi tình huống, phân biệt 401 và 403, 400 và 422, cùng anti-pattern 200-OK-với-error-body và quyết định áp dụng cho Shop API.

12/06/2026
10 phút đọc
0 lượt xem
1

Mục Tiêu Bài Học

Sau bài học, bạn sẽ:

  • Biết 5 class status code (1xx, 2xx, 3xx, 4xx, 5xx) và ý nghĩa của mỗi class theo RFC 9110.
  • Nắm top 20 code phổ biến nhất trong REST API, gồm 200, 201, 204, 301, 302, 303, 304, 307, 308, 400, 401, 403, 404, 405, 409, 422, 429, 500, 502, 503, 504.
  • Chọn đúng code cho mỗi tình huống: success thường, success không body, redirect, client error do validate, client error do auth, server error.
  • Phân biệt được hai cặp dễ nhầm: 401 với 403, 400 với 422.
  • Tránh anti-pattern "200 OK với error body trong JSON" và hiểu vì sao pattern này phá vỡ cache cùng monitoring.
  • Áp dụng quy ước status code vào Shop API endpoints (CRUD products, orders, cart) và liên kết với AppError enum sẽ xuất hiện ở G16.
2

5 Class Status Code Toàn Cảnh

RFC 9110 (HTTP Semantics, năm 2022, thay thế RFC 7231) định nghĩa status code là số ba chữ số mà digit đầu chia toàn bộ không gian code thành 5 class. Digit đầu cho client biết category của response trước khi đọc body, đủ thông tin để quyết định retry, redirect, fail fast hay parse tiếp:

Class   Tên              Ý nghĩa
1xx     Informational    Request đã nhận, tiếp tục xử lý (hiếm gặp trong REST)
2xx     Successful       Request xử lý OK
3xx     Redirection      Cần thực hiện thêm hành động (thường là redirect)
4xx     Client Error     Client gửi sai (bad request, unauthorized, not found, ...)
5xx     Server Error     Server xử lý fail (internal error, gateway timeout, ...)

1xx Informational rất ít gặp trong REST API. Ví dụ điển hình là 100 Continue dùng khi client gửi header Expect: 100-continue để hỏi server có chấp nhận body lớn không trước khi tải lên thật, hoặc 101 Switching Protocols cho upgrade HTTP sang WebSocket. Phần lớn handler ứng dụng không trực tiếp trả 1xx — đó là việc của HTTP layer bên dưới.

Bốn class còn lại — 2xx, 3xx, 4xx, 5xx — phủ gần như toàn bộ response thực tế và là trọng tâm của bài này. Quy tắc cốt lõi: status code là source-of-truth cho biết request đã thành công hay thất bại; body chỉ kèm thêm chi tiết. Sai status code đồng nghĩa phá vỡ contract HTTP với toàn bộ proxy, cache, monitoring tool nằm giữa client và server.

3

2xx Success — Phổ Biến Nhất

2xx báo cho client biết request đã được nhận, hiểu, và xử lý thành công. Bốn code 2xx cần nắm chắc:

200 OK — generic success. Dùng cho GET trả về resource, PUT thay thế xong và muốn trả lại entity mới, PATCH cập nhật xong và trả entity sau khi sửa. Đây là code mặc định cho mọi success có body.

GET /api/v1/products/43 HTTP/1.1
Host: shop.example.com

HTTP/1.1 200 OK
Content-Type: application/json

{"id": 43, "name": "Laptop Y", "price": 1499.00}

201 Created — POST tạo resource mới thành công. Phải kèm header Location trỏ tới URL của resource vừa tạo để client navigate tiếp được. Body thường chứa biểu diễn đầy đủ của resource mới.

POST /api/v1/products HTTP/1.1
Host: shop.example.com
Content-Type: application/json

{"name": "Laptop Y", "price": 1499.00}

HTTP/1.1 201 Created
Location: /api/v1/products/43
Content-Type: application/json

{"id": 43, "name": "Laptop Y", "price": 1499.00}

202 Accepted — server đã nhận request nhưng chưa xử lý xong, sẽ làm bất đồng bộ (background job, message queue). Response thường kèm URL hoặc job ID để client polling tiến trình. Phù hợp cho export báo cáo, gửi email hàng loạt, video transcode.

POST /api/v1/admin/reports/export HTTP/1.1

HTTP/1.1 202 Accepted
Location: /api/v1/admin/reports/jobs/job_8f3c
Content-Type: application/json

{"job_id": "job_8f3c", "status": "queued"}

204 No Content — success nhưng không kèm body. Dùng nhiều nhất cho DELETE thành công, và đôi khi cho PUT replace (khi server không muốn trả lại resource). Response 204 không được phép có body theo RFC 9110 mục 15.3.5; bất kỳ byte nào sau header sẽ bị client coi là sai.

DELETE /api/v1/cart/items/15 HTTP/1.1
Authorization: Bearer eyJhbGc...

HTTP/1.1 204 No Content

Áp dụng vào Shop API theo CRUD mapping đã lock ở bài 2: POST /api/v1/products trả 201 kèm Location; GET /api/v1/products/:id trả 200; PUT /api/v1/products/:id trả 200 hoặc 204; PATCH /api/v1/products/:id trả 200; DELETE /api/v1/cart/items/:item_id trả 204. POST /api/v1/admin/reports/export sau này sẽ trả 202 vì export chạy qua background worker.

4

3xx Redirection

3xx báo client cần thêm hành động — thường là gửi lại request tới URL khác. Tất cả code 3xx kèm header Location chỉ điểm đến mới. Sáu code cần nắm:

301 Moved Permanently — URL cũ đã đổi vĩnh viễn sang URL mới. Search engine cập nhật index, browser và proxy cache mạnh tay. Dùng khi đổi slug product, đổi domain, dọn cấu trúc URL legacy.

302 Found — redirect tạm thời. Đây là code lịch sử để lại: trong HTTP/1.0 nó được hiểu là "tạm thời", nhưng nhiều browser sai chuẩn đổi POST thành GET khi follow. RFC 9110 khuyến nghị dùng 303 hoặc 307 thay thế cho rõ ngữ nghĩa.

303 See Other — sau khi server xử lý POST, redirect client tới một URL GET khác. Đây là nền tảng của post-redirect-get pattern chống double-submit khi user refresh form: form POST tới /orders, server lưu order rồi trả 303 với Location: /orders/123; browser tự GET trang detail.

304 Not Modified — response cho conditional GET. Client gửi If-None-Match: "etag-xyz" hoặc If-Modified-Since; nếu resource chưa đổi, server trả 304 không có body, client dùng bản cache local. Đây là cơ chế cache validation chính của HTTP.

GET /api/v1/products/43 HTTP/1.1
If-None-Match: "a1b2c3d4"

HTTP/1.1 304 Not Modified
ETag: "a1b2c3d4"

307 Temporary Redirect308 Permanent Redirect — phiên bản nghiêm ngặt của 302 và 301: preserve method. Client phải gửi lại đúng method ban đầu sang URL mới — POST vẫn là POST. Đây là điểm khác biệt cốt lõi với 301/302 (cho phép browser đổi method) và với 303 (cố ý đổi sang GET). Khi cần redirect API mà giữ ngữ nghĩa method, chọn 307/308.

5

4xx Client Error — Validate & Auth

4xx báo client đã gửi sai. Sửa lại request là việc của client; retry mù không giải quyết được. Tám code 4xx chiếm hầu hết trường hợp:

400 Bad Request — request không parse được. JSON syntax sai, thiếu dấu phẩy, body rỗng khi method yêu cầu body, query param không đúng định dạng. Dùng khi server không hiểu nội dung gửi tới chứ chưa kịp validate business.

401 Unauthorized — client chưa authenticate. Tên gọi RFC dễ gây hiểu lầm: 401 thực ra là "not authenticated" (chưa cung cấp danh tính, hoặc cung cấp sai), không phải "không có quyền". Khi user chưa login hoặc token hết hạn, trả 401. Response nên kèm header WWW-Authenticate chỉ scheme chấp nhận.

403 Forbidden — client đã authenticated nhưng không có quyền truy cập resource cụ thể. User thường gọi endpoint admin, hoặc đụng resource thuộc về user khác — trả 403. Đây mới đúng nghĩa "no permission".

404 Not Found — resource không tồn tại. Một mẹo bảo mật: thay vì trả 403 khi user không có quyền xem resource bí mật (vô tình tiết lộ rằng resource đó tồn tại), trả 404 để giấu hoàn toàn sự hiện diện — common practice của GitHub repos private, Google Drive file riêng.

405 Method Not Allowed — endpoint tồn tại nhưng method sai. Ví dụ gọi POST /api/v1/products/:id trong khi route chỉ chấp nhận GET/PUT/PATCH/DELETE. Response phải kèm header Allow liệt kê method hợp lệ.

POST /api/v1/products/43 HTTP/1.1

HTTP/1.1 405 Method Not Allowed
Allow: GET, PUT, PATCH, DELETE

409 Conflict — request hợp lệ nhưng đụng state hiện tại của server. Hai use case chính: duplicate (đăng ký với email đã tồn tại), và version conflict trong optimistic locking (sẽ học ở B78). Shop API trả 409 khi user register email trùng, hoặc khi PATCH product với version cũ hơn version hiện tại.

422 Unprocessable Entity — request parse được nhưng vi phạm business rule. JSON đúng cú pháp, field đủ, nhưng giá âm, email sai định dạng, số lượng cart vượt stock. Đây là code dành cho validation error per-field, thường trả kèm body liệt kê field nào sai.

429 Too Many Requests — client vượt rate limit. Response phải kèm header Retry-After chỉ số giây client nên đợi trước khi thử lại. Shop API sẽ dùng 429 cho rate limit theo IP (login brute-force) và theo user (gọi API quá nhanh).

HTTP/1.1 429 Too Many Requests
Retry-After: 60
Content-Type: application/json

{"error": "Rate limit exceeded", "code": "RATE_LIMITED"}

Lưu ý 400 vs 422: phân biệt theo nguyên tắc parse fail vs business fail. 400 cho JSON broken, syntax sai, body trống. 422 cho JSON parse OK nhưng giá trị sai business (giá âm, slug đã dùng, stock vượt limit). Lập trường này rõ ràng và nhất quán; nhiều framework cũ trả 400 cho cả hai khiến client khó phân biệt nguyên nhân.

6

5xx Server Error — Backend Failed

5xx báo server đã fail dù client gửi đúng. Đây là lỗi phía server, client chỉ có thể retry (với backoff) hoặc fail nhẹ nhàng cho user. Bốn code 5xx cần nắm:

500 Internal Server Error — generic, cái gì đó vỡ bên trong. Code panic trong handler, query database sai, thư viện ngoài throw exception ngoài dự tính, lỗi logic chưa cover. 500 là code "tôi cũng không biết chính xác chuyện gì", và xuất hiện càng nhiều càng cảnh báo code chưa được phòng lỗi tốt.

502 Bad Gateway — server đứng giữa (reverse proxy, load balancer, API gateway) nhận response invalid từ upstream. Ví dụ: nginx gọi axum app nhưng app crash giữa chừng, nginx nhận TCP RST hoặc HTTP malformed; nginx trả 502 cho client. Là dấu hiệu upstream chết hoặc network giữa middleware và app có vấn đề.

503 Service Unavailable — server quá tải hoặc đang maintenance. Khác 502 ở chỗ server biết mình đang busy/down chứ không phải upstream lỗi. Response nên kèm Retry-After. Load balancer trả 503 khi tất cả backend healthcheck fail; app tự trả 503 khi tự đặt vào maintenance mode.

504 Gateway Timeout — server đứng giữa timeout chờ upstream. Khác 502: 502 là nhận được response sai, 504 là không nhận được gì trong thời gian cho phép. Thường gặp khi DB query quá lâu, external API treo, hoặc deadlock.

HTTP/1.1 503 Service Unavailable
Retry-After: 30
Content-Type: application/json

{"error": "Service temporarily unavailable", "code": "MAINTENANCE"}

Nguyên tắc quan trọng với 5xx: không expose stack trace ra client. Log đầy đủ chi tiết (file, line, backtrace, request ID) vào hệ thống tracing internal; response client chỉ trả message generic kèm code phân loại và request_id để support tra cứu. Lộ stack trace giúp attacker map cấu trúc thư mục, version thư viện, query format — thông tin vàng cho recon.

7

Cách Chọn Đúng Code — Decision Flow

Khi viết handler, dòng tư duy chọn status code đi theo thứ tự dưới đây. Validate trước, process sau, error cuối:

Request đến → Validate input
  ↓ parse fail              → 400
  ↓ business rule fail      → 422
  ↓ missing auth            → 401
  ↓ no permission           → 403
  ↓ resource not exist      → 404
  ↓ method wrong            → 405
  ↓ duplicate/conflict      → 409
  ↓ too fast                → 429
Process
  ↓ create OK               → 201 + Location
  ↓ delete OK               → 204
  ↓ partial OK              → 200 + body
  ↓ async accepted          → 202
Internal fail
  ↓ panic/uncaught          → 500
  ↓ DB timeout              → 504
  ↓ overload                → 503

Áp dụng cụ thể cho từng endpoint Shop API. POST /api/v1/auth/register: body sai cú pháp trả 400; email không đúng định dạng trả 422; email đã tồn tại trả 409; tạo user OK trả 201 kèm Location: /api/v1/users/<id>. GET /api/v1/products/:id: id không phải UUID trả 400; product không tồn tại trả 404; OK trả 200. DELETE /api/v1/cart/items/:item_id: chưa login trả 401; item thuộc user khác trả 403 (hoặc 404 nếu muốn giấu); xoá OK trả 204. POST /api/v1/checkout: cart rỗng trả 422; stock không đủ trả 409; payment service timeout trả 504.

Quan trọng nhất là tính nhất quán: cùng một loại lỗi phải trả cùng một code khắp API. Việc này không thể đảm bảo bằng review thủ công — phải implement qua kiểu dữ liệu, sẽ là vai trò của AppError enum khi bước vào tầng axum.

8

Anti-Pattern: 200 OK Với Error Body

Một anti-pattern xuất hiện đều đặn trong code base cũ là trả 200 OK cho mọi response, bất kể thành công hay thất bại, và đặt thông tin lỗi vào body JSON:

HTTP/1.1 200 OK
Content-Type: application/json

{
  "success": false,
  "error": "Product not found",
  "data": null
}

Cách đúng là dùng status code làm source-of-truth; body chỉ kèm chi tiết error:

HTTP/1.1 404 Not Found
Content-Type: application/json

{
  "error": "Product not found",
  "code": "PRODUCT_NOT_FOUND",
  "request_id": "req_8f3c2a1b"
}

Vì sao "200 với error body" là sai? Bốn lý do thực tế:

  • Break caching: cache layer (Varnish, Cloudflare, browser cache) coi 200 là cacheable. Response lỗi bị cache, mọi user tiếp theo nhận lại error cache cho tới hết TTL. Status 4xx/5xx mặc định không cache.
  • Break monitoring: nginx access log, Cloudflare analytics, Prometheus exporter đếm 200 là healthy. Service đang fail liên tục vẫn show "100% success rate" trên dashboard. Alert không bao giờ trigger.
  • Logic client phức tạp: client buộc phải parse body để xác định success/fail thay vì chỉ kiểm tra response.ok hay status_code < 400. Mỗi handler client cần một wrapper riêng.
  • Tool ecosystem không cooperate: curl --fail, fetch().ok, reqwest::Response::error_for_status() đều dựa trên status code. Convention này lỡ thì mọi tool generic đều dùng sai.

Áp dụng Shop API: AppError enum trong shop-common::error (sẽ define ở B16) sẽ map mỗi variant sang HTTP status đúng — AppError::NotFound → 404, AppError::Unauthenticated → 401, AppError::Forbidden → 403, AppError::Validation → 422, AppError::Conflict → 409, AppError::Internal → 500. Mọi handler trong shop-api sẽ trả Result<T, AppError>; trait IntoResponse tự render thành HTTP response với đúng code và body envelope chuẩn.

Ngoại lệ duy nhất đáng nhắc: GraphQL trả 200 kèm mảng errors cho cả lỗi business — đó là protocol khác, không phải REST, và đặt trong context "single endpoint, query language" của riêng GraphQL. Series này build REST nên Shop API sẽ giữ status code là source-of-truth tuyệt đối.

9

Tổng Kết

  • 5 class status code theo RFC 9110: 1xx informational (hiếm gặp), 2xx success, 3xx redirection, 4xx client error, 5xx server error.
  • 2xx phổ biến: 200 (generic success có body), 201 (POST create kèm Location), 202 (async accepted), 204 (success không body).
  • 3xx phổ biến: 301 (permanent move), 303 (post-redirect-get), 304 (cache valid với ETag), 307/308 (preserve method khi redirect).
  • 4xx phổ biến: 400 (parse fail), 401 (not authenticated), 403 (no permission), 404 (resource not exist), 405 (method sai), 409 (conflict/duplicate), 422 (business validation fail), 429 (rate limit, kèm Retry-After).
  • 5xx phổ biến: 500 (generic, code bug/panic), 502 (upstream sai), 503 (overload/maintenance), 504 (upstream timeout). Không expose stack trace ra client — chỉ log internal.
  • 401 là "not authenticated" (sai tên theo trực giác), 403 mới đúng nghĩa "no permission". Cặp dễ nhầm.
  • 400 vs 422: 400 cho parse fail; 422 cho parse OK nhưng vi phạm business rule. Cặp dễ nhầm thứ hai.
  • Anti-pattern: "200 OK với error body" phá vỡ cache, phá vỡ monitoring (nginx/Cloudflare đếm 200 là healthy), buộc client parse body để biết success/fail, không cooperate với tool ecosystem.
  • Shop API quy ước status code là source-of-truth tuyệt đối. AppError enum (chi tiết ở B16/G16) sẽ map mỗi variant sang HTTP status đúng qua impl IntoResponse.
10

Bài Tập Củng Cố

Tự trả lời, đáp án ở cuối:

  1. Phân biệt 401 và 403. Khi user gửi request không kèm token, server nên trả code nào? Khi user đã login nhưng cố truy cập endpoint admin?
  2. Khi nào dùng 400 và khi nào dùng 422? Cho hai ví dụ cụ thể từ Shop API ứng với mỗi code.
  3. POST /api/v1/products tạo product thành công nên trả status code gì? Header nào phải đính kèm và giá trị header đó là gì?
  4. DELETE /api/v1/cart/items/:item_id thành công nên trả status code gì? Response có body không? Vì sao?
  5. Vì sao "200 OK với error body" là anti-pattern? Liệt kê ít nhất ba thứ pattern này phá vỡ và cho ví dụ cụ thể.
Đáp án
  1. 401 nghĩa "not authenticated" — client chưa cung cấp danh tính hoặc danh tính sai (thiếu token, token hết hạn, token signature invalid). 403 nghĩa "authenticated nhưng không có quyền truy cập resource cụ thể". Request không kèm token → 401 (response nên có header WWW-Authenticate: Bearer chỉ scheme yêu cầu). User customer login đầy đủ nhưng gọi POST /api/v1/admin/products → 403 (đã xác định danh tính rồi, vấn đề là role không đủ).
  2. 400 cho request không parse được; 422 cho request parse OK nhưng business rule fail. Ví dụ 400: body của POST /api/v1/products là JSON thiếu dấu phẩy {"name": "X" "price": 100}, hoặc body rỗng khi handler yêu cầu Content-Type JSON, hoặc query param ?page=abc không parse được sang số. Ví dụ 422: body parse OK nhưng price âm, slug đã tồn tại trong DB, quantity trong cart vượt stock hiện có, email register sai định dạng. Mọi response 422 nên kèm body liệt kê field nào sai và lý do, ví dụ {"errors": [{"field": "price", "message": "must be positive"}]}.
  3. Status code 201 Created. Header bắt buộc là Location: /api/v1/products/<id_mới> trỏ tới URL của product vừa tạo để client navigate hoặc cache tham chiếu. Response body thường kèm representation đầy đủ của product mới (id, name, price, stock, created_at, ...). Trong Shop API, endpoint create dành cho admin sẽ là POST /api/v1/admin/products trả 201 cùng Location: /api/v1/admin/products/<id>.
  4. Status code 204 No Content. Response không có body và không được phép có body theo RFC 9110 mục 15.3.5 — bất kỳ byte nào sau header sẽ bị client coi là sai protocol. Lý do dùng 204: DELETE thành công không có gì hữu ích để trả về (resource đã không tồn tại), client chỉ cần biết thao tác OK. Nếu muốn trả lại representation cuối của resource trước khi xoá (use case audit/undo), có thể chọn 200 OK + body — đó là lựa chọn design, nhưng convention phổ biến là 204.
  5. Pattern phá vỡ ít nhất bốn thứ. (1) Cache: cache layer (Varnish, Cloudflare, browser) coi 200 là cacheable, response error bị cache và phục vụ lại cho mọi user tiếp theo trong TTL; status 4xx/5xx mặc định không cache. (2) Monitoring: nginx access log, Cloudflare analytics, Prometheus exporter đếm 200 là healthy → dashboard hiển thị "100% success rate" trong khi service đang fail, alert không trigger. (3) Logic client: thay vì check response.ok hoặc status < 400, client phải parse body và check field như success hoặc error, nhân lên qua mỗi handler. (4) Tool ecosystem: curl --fail, fetch().ok, reqwest::Response::error_for_status() đều dựa status code; pattern này khiến mọi tool generic dùng sai. Ví dụ thực tế: API trả 200 với {"success": false, "error": "DB down"} trong khi DB chết, Cloudflare report uptime 100%, on-call kỹ sư không nhận alert suốt nửa ngày.
11

Bài Tiếp Theo

— đi sâu vào các header quan trọng nhất cho REST API (Content-Type, Accept, Authorization, Cookie, User-Agent, Cache-Control, ETag, X-Request-Id, X-Forwarded-For), phân biệt request vs response header, quy ước custom header prefix X-.