DEV Community

Cover image for Thực hành tốt nhất xử lý lỗi REST API: Mã trạng thái, RFC 9457 và Lỗi có thể thử lại
Sebastian Petrus
Sebastian Petrus

Posted on Originally published at apidog.com

Thực hành tốt nhất xử lý lỗi REST API: Mã trạng thái, RFC 9457 và Lỗi có thể thử lại

Thiết kế hợp đồng lỗi cho API REST: mã trạng thái, RFC 9457 và khả năng thử lại

Phản hồi lỗi là một phần của hợp đồng API. Client dùng chúng để phân nhánh logic, cơ chế thử lại dựa vào chúng, còn đội ngũ hỗ trợ cần chúng để điều tra sự cố. Tuy nhiên, nhiều nhóm chỉ thiết kế luồng thành công rồi để framework tự tạo lỗi mặc định. Kết quả là API có nhiều định dạng lỗi, phản hồi 200 chứa "success": false, hoặc tệ hơn là stack trace làm lộ cấu trúc cơ sở dữ liệu.

Dùng thử Apidog ngay hôm nay

Hướng dẫn này trình bày cách xây dựng hợp đồng lỗi nhất quán cho dịch vụ REST:

  • Chọn mã trạng thái HTTP phù hợp.
  • Chuẩn hóa nội dung bằng RFC 9457 Problem Details.
  • Tách mã lỗi máy đọc được khỏi thông báo dành cho con người.
  • Đánh dấu lỗi có thể thử lại.
  • Không để lộ thông tin nhạy cảm.
  • Kiểm thử mọi luồng thất bại trong Apidog.

Các nguyên tắc dưới đây bổ sung cho phân tích về những mã trạng thái HTTP REST API nên sử dụng.

1. Chọn mã trạng thái trước khi thiết kế nội dung lỗi

HTTP đã cung cấp lớp ngữ nghĩa lỗi đầu tiên:

  • 4xx: client gửi yêu cầu không hợp lệ hoặc không được phép. Gửi lại cùng yêu cầu thường vẫn thất bại.
  • 5xx: server hoặc dependency gặp lỗi. Yêu cầu của client có thể hoàn toàn hợp lệ.

Client, proxy, cache và thư viện retry thường phân nhánh dựa trên status code mà không cần đọc JSON. Vì vậy, hãy xác định mã trạng thái trước khi viết schema lỗi. Có thể tham khảo HTTP status code trên MDN và dùng bảng quyết định sau:

Tình huống Nên dùng Không nên dùng Lý do
JSON hỏng, Content-Type sai hoặc thiếu trường bắt buộc 400 Bad Request 422 Server không thể phân tích cú pháp hoặc hiểu yêu cầu
Yêu cầu đúng cú pháp nhưng vi phạm quy tắc nghiệp vụ, chẳng hạn số tiền âm 422 Unprocessable Content 400 Cú pháp hợp lệ nhưng giá trị không hợp lệ
Thiếu thông tin xác thực hoặc token hết hạn/không hợp lệ 401 Unauthorized 403 Client chưa chứng minh được danh tính; gửi WWW-Authenticate
Đã xác thực nhưng không đủ quyền 403 Forbidden 401 Danh tính đã biết nhưng quyền truy cập bị từ chối
Tài nguyên không tồn tại hoặc bạn không muốn xác nhận tài nguyên tồn tại 404 Not Found 410 Mặc định an toàn và giúp hạn chế rò rỉ thông tin
Tài nguyên từng tồn tại nhưng đã bị xóa vĩnh viễn 410 Gone 404 Thông báo cho client và crawler xóa tham chiếu
Xung đột trạng thái: khóa trùng, phiên bản cũ hoặc xung đột chỉnh sửa 409 Conflict 400 Yêu cầu hợp lệ nhưng xung đột với trạng thái hiện tại
Client vượt giới hạn tốc độ 429 Too Many Requests 503 Kèm Retry-After để client lùi lại đúng cách
Ngoại lệ chưa được xử lý trong server 500 Internal Server Error 502 Chính server của bạn gặp lỗi
Upstream trả dữ liệu không hợp lệ cho gateway 502 Bad Gateway 500 Lỗi nằm ở dependency phía sau gateway
Server quá tải hoặc đang bảo trì 503 Service Unavailable 500 Lỗi tạm thời; thêm Retry-After nếu có thể
Upstream hết thời gian chờ 504 Gateway Timeout 500 Phân biệt dependency chậm với lỗi trong mã server

Hai ranh giới cần đặc biệt chú ý

401403 không phải lựa chọn về phong cách. Đây là ranh giới bảo mật. Trả về 403 cho một request chưa xác thực có thể xác nhận rằng tài nguyên tồn tại, từ đó hỗ trợ việc dò quét trái phép.

Tương tự, 429 không có Retry-After khiến client tiếp tục gửi request trong vòng lặp chặt. Nếu API có rate limit, hãy kết hợp status code với tín hiệu lùi lại cụ thể. Xem thêm cách triển khai API rate limiting.

2. Chuẩn hóa bằng RFC 9457 Problem Details

Sau khi chọn đúng status code, mọi lỗi nên dùng cùng một media type và schema:

application/problem+json
Enter fullscreen mode Exit fullscreen mode

RFC 9457 định nghĩa năm trường cốt lõi:

  • type: URI xác định loại lỗi.
  • title: tiêu đề ngắn, dễ hiểu.
  • status: mã HTTP, được lặp lại để thuận tiện.
  • detail: mô tả lỗi trong lần xảy ra hiện tại.
  • instance: URI định danh instance lỗi cụ thể.

Các trường khác là extension do API tự định nghĩa. RFC 9457 mô tả đầy đủ đặc tả; bài giải thích RFC 9457 đi sâu hơn vào từng thành phần và cách RFC 9457 thay thế RFC 7807.

Ví dụ lỗi xác thực trên endpoint thanh toán:

POST /v1/payments HTTP/1.1
Content-Type: application/json

{ "amount": -1400, "currency": "USD", "source": "card_8xKt2" }
Enter fullscreen mode Exit fullscreen mode
HTTP/1.1 422 Unprocessable Content
Content-Type: application/problem+json

{
  "type": "https://api.example.com/problems/validation-error",
  "title": "Request validation failed",
  "status": 422,
  "detail": "One or more fields failed validation.",
  "instance": "/v1/payments/requests/req_9f3c1a7b",
  "code": "PAYMENT_VALIDATION_FAILED",
  "errors": [
    {
      "field": "amount",
      "code": "AMOUNT_NOT_POSITIVE",
      "message": "amount must be a positive integer in minor units"
    }
  ],
  "request_id": "req_9f3c1a7b"
}
Enter fullscreen mode Exit fullscreen mode

errors[] là extension hữu ích cho lỗi cấp trường. Frontend có thể ánh xạ từng lỗi vào đúng input thay vì hiển thị một thông báo chung. Chọn một quy ước đường dẫn, chẳng hạn JSON Pointer hoặc dotted path, rồi áp dụng nhất quán.

Quan trọng nhất: dùng Problem Details cho mọi lỗi, kể cả lỗi do framework, reverse proxy hoặc gateway tạo ra. Nếu handler ứng dụng trả JSON nhưng load balancer trả HTML cho lỗi 502, client vẫn phải duy trì hai parser.

3. Tách mã lỗi và thông báo

Ví dụ trên có cả codemessage vì chúng phục vụ hai đối tượng khác nhau.

Mã máy đọc được

Các mã như sau là một phần của contract:

AMOUNT_NOT_POSITIVE
CURRENCY_UNSUPPORTED
IDEMPOTENCY_KEY_REUSED
Enter fullscreen mode Exit fullscreen mode

Client có thể dùng chúng để phân nhánh logic, nên mã cần:

  • Ổn định.
  • Được ghi trong tài liệu.
  • Có thể liệt kê.
  • Không thay đổi ý nghĩa tùy tiện.

Đừng buộc client phân tích văn xuôi:

if (message.includes("positive")) {
  // fragile
}
Enter fullscreen mode Exit fullscreen mode

Chỉ một lần chỉnh sửa câu chữ cũng có thể trở thành breaking change.

Thông báo dành cho con người

Thông báo nên giúp nhà phát triển hiểu lỗi và biết cách sửa:

Số tiền phải là số nguyên dương theo đơn vị nhỏ.
Enter fullscreen mode Exit fullscreen mode

Thông báo này hữu ích hơn:

Số tiền không hợp lệ.
Enter fullscreen mode Exit fullscreen mode

Bạn có thể cải thiện hoặc bản địa hóa message bất cứ lúc nào, miễn là giữ nguyên code. Sự tách biệt này cũng quan trọng với các client tự động và client dựa trên LLM, vốn phục hồi tốt hơn từ lỗi có cấu trúc và tự mô tả. Xem thêm thiết kế thông báo lỗi API cho AI agent.

4. Không đưa thông tin nhạy cảm vào phản hồi lỗi

Lỗi chưa được xử lý là một kênh trinh sát phổ biến. Middleware xử lý lỗi phải loại bỏ các thông tin sau khỏi response:

  • Stack trace, tên lớp và đường dẫn file.
  • Câu SQL, query fragment và lỗi ORM.
  • Hostname nội bộ, IP, port và tên service.
  • Phiên bản thư viện hoặc banner của framework.
  • Secret, token và connection string trong exception.
  • Việc một tài khoản có tồn tại hay không, đặc biệt trong đăng nhập và đặt lại mật khẩu.

Mô hình an toàn:

  1. Bắt mọi exception tại ranh giới ứng dụng.
  2. Ghi đầy đủ exception ở phía server.
  3. Gắn log với request ID.
  4. Trả về Problem Details tổng quát cho client.
{
  "detail": "An internal error occurred",
  "request_id": "req_51ad0"
}
Enter fullscreen mode Exit fullscreen mode

Client chỉ thấy thông tin cần thiết; log server vẫn giữ đủ dữ liệu để điều tra.

5. Đánh dấu lỗi có thể thử lại

Mỗi response lỗi đều trả lời câu hỏi: client có nên gửi lại request không?

Theo ngữ nghĩa mặc định:

  • 429, 502, 503, 504: thường có thể retry bằng exponential backoff và jitter.
  • 500: có thể retry thận trọng, thường chỉ một số lần giới hạn.
  • Hầu hết 4xx khác: lỗi cuối cùng; retry cùng request sẽ tiếp tục thất bại.

Timeout cần được xử lý cẩn thận. Request có thể đã thành công sau khi client ngừng chờ. Với endpoint làm thay đổi trạng thái, hãy hỗ trợ idempotency key để retry thanh toán không gây tính phí hai lần. 408 Request Timeout là trường hợp kinh điển của vấn đề này.

Bạn cũng có thể biểu diễn rõ khả năng retry bằng extension:

{
  "type": "https://api.example.com/problems/rate-limited",
  "title": "Too many requests",
  "status": 429,
  "code": "RATE_LIMITED",
  "retryable": true,
  "retry_after_seconds": 30
}
Enter fullscreen mode Exit fullscreen mode

retryable cho phép ghi đè quy tắc mặc định. Ví dụ, một loại lỗi 500 cụ thể có thể là lỗi cuối cùng vì retry sẽ làm hỏng trạng thái. Ghi lại quy ước này một lần để mọi SDK có cùng hành vi.

6. Thêm request ID và quản lý phiên bản contract

Request ID và trace ID

Gán ID cho mỗi request:

  • Chấp nhận X-Request-Id nếu có hoặc tự tạo ID mới.
  • Ghi ID vào mọi dòng log.
  • Trả lại ID trong mọi lỗi dưới dạng request_id.
  • Trong hệ thống phân tán, truyền thêm traceparent của W3C.

Khi khách hàng gửi lỗi trong ticket hỗ trợ, request ID biến việc tìm log từ một giờ thành một truy vấn duy nhất.

Versioning

Hãy quản lý contract lỗi như quản lý API:

Thường an toàn:

  • Thêm extension mới.
  • Thêm mã lỗi mới.
  • Thêm trường tùy chọn.

Có thể là breaking change:

  • Đổi tên errors[].field.
  • Thay đổi ý nghĩa của một code.
  • Chuyển từ schema tùy chỉnh sang Problem Details mà không có kế hoạch tương thích.

Giữ URI trong type ổn định lâu dài. Khi ngữ nghĩa thay đổi, giới thiệu URI mới thay vì thay đổi ý nghĩa URI cũ. Tài liệu cũng nên quy định rằng client phải bỏ qua extension hoặc mã lỗi chưa biết, thay vì coi đó là lỗi không thể xử lý. Đây là điều giúp contract phát triển mà không cần ngay lập tức phát hành API v2.

7. Kiểm thử mọi luồng lỗi trong Apidog

Contract lỗi sẽ nhanh chóng lỗi thời nếu không được kiểm thử. Luồng thành công thường xuất hiện trong mọi demo; các nhánh 401, 422 hoặc 503 chỉ xuất hiện khi khách hàng gặp sự cố.

Hãy biến các trường hợp thất bại thành test case hạng nhất.

Kiểm thử phía server

Với mỗi endpoint, tạo test cho từng lỗi quan trọng:

  • Thiếu xác thực → 401.
  • Không đủ quyền → 403.
  • Số tiền âm → 422, errors[0].code = AMOUNT_NOT_POSITIVE.
  • Lưu lượng vượt giới hạn → 429 và có Retry-After.
  • Dependency không phản hồi → 504.
  • Xung đột ghi dữ liệu → 409.

Các assertion nên kiểm tra:

  • Status code.
  • Response headers.
  • Content-Type.
  • Các trường bắt buộc.
  • Giá trị code.
  • JSON Schema của Problem Details.

API assertions của Apidog giúp đưa các kiểm tra này vào CI, để schema lỗi sai bị phát hiện trước khi lên production.

Mock cho frontend và SDK

Frontend và SDK không nên phải chờ backend tạo lỗi thật mới bắt đầu tích hợp. Dùng mock server để mô phỏng:

  • 503 với Retry-After: 120.
  • 409 khi gửi request trùng.
  • Payload 422 có đầy đủ errors[].
  • 401403 với các thông báo khác nhau.

Mock server của Apidog có thể trả về Problem Details từ đặc tả API, giúp kiểm tra giao diện, thông báo và logic retry mà không cần tự viết Express stub hoặc cố tình làm hỏng backend.

Kết luận

Một contract lỗi tốt cần nhất quán từ HTTP status code đến JSON schema:

  1. Chọn status code theo ngữ nghĩa HTTP.
  2. Dùng application/problem+json cho mọi lỗi.
  3. Tách code ổn định khỏi message dành cho con người.
  4. Không trả stack trace hoặc thông tin nội bộ.
  5. Khai báo khả năng retry và thời gian chờ.
  6. Gắn request ID vào response và log.
  7. Kiểm thử cả server lẫn client trong CI.

Bạn có thể tải Apidog và dùng thử miễn phí. Nhập một đặc tả OpenAPI hiện có để tạo các response lỗi có thể mock chỉ trong vài phút.

Câu hỏi thường gặp

Nên dùng 400 hay 422 cho lỗi xác thực?

Dùng 400 khi request bị định dạng sai hoặc server không thể hiểu request: JSON không hợp lệ, Content-Type sai hoặc thiếu trường bắt buộc.

Dùng 422 khi request đã được phân tích cú pháp nhưng giá trị vi phạm quy tắc nghiệp vụ, chẳng hạn số tiền âm hoặc currency không được hỗ trợ.

Lợi ích là giúp client chẩn đoán chính xác: 422 có nghĩa “hãy sửa dữ liệu”, còn 400 có nghĩa “hãy sửa định dạng request”. Dù chọn cách phân chia nào, hãy áp dụng nhất quán trên mọi endpoint.

application/problem+json là gì?

Đây là media type do RFC 9457 định nghĩa cho Problem Details, một định dạng lỗi JSON tiêu chuẩn của HTTP API. Response thường chứa type, title, status, detailinstance, cùng các extension như errors[].

Media type đã đăng ký giúp middleware và client chung nhận diện response lỗi mà không cần cấu hình riêng.

Client nên tự động retry những lỗi HTTP nào?

Retry 429, 502, 503504 bằng exponential backoff kết hợp jitter, đồng thời tôn trọng Retry-After nếu có.

Có thể retry 500 một lần một cách thận trọng. Không nên retry các 4xx khác vì request sẽ tiếp tục thất bại. Với endpoint thay đổi trạng thái, luôn kết hợp retry với idempotency key để tránh thao tác trùng hoặc tính phí hai lần.

Làm thế nào để kiểm thử lỗi mà không làm hỏng backend?

Hãy mô phỏng lỗi. Trỏ client đến mock server trả về các response 4xx5xx từ đặc tả API, sau đó kiểm tra cách client hiển thị và retry.

Ở phía server, tạo test với payload không hợp lệ, request thiếu xác thực, xung đột dữ liệu và lưu lượng đột biến. Xác nhận status code, headers và schema response. Chạy cả hai nhóm test trong CI để contract lỗi luôn được kiểm chứng.

Top comments (0)