Các phản hồi lỗi của API là một phần của hợp đồng API. Các client phân tích chúng, logic thử lại phân nhánh dựa trên chúng, và các kỹ sư hỗ trợ tìm kiếm chúng lúc 2 giờ sáng. Tuy nhiên, hầu hết các nhóm chỉ thiết kế chi tiết luồng thành công và để lỗi tự phát sinh từ những gì framework làm mặc định. Đó là cách bạn kết thúc với ba dạng lỗi khác nhau trong một API, một phản hồi 200 chứa "success": false, và một dấu vết stack làm lộ sơ đồ cơ sở dữ liệu của bạn ra internet công cộng.
Hướng dẫn này bao gồm các phương pháp hay nhất về xử lý lỗi API cho các dịch vụ REST từ đầu đến cuối: chọn mã trạng thái phù hợp, tiêu chuẩn hóa một định dạng phản hồi lỗi với RFC 9457 Problem Details, tách biệt mã lỗi máy đọc được khỏi thông báo người dùng, đánh dấu lỗi có thể thử lại và giữ bí mật khỏi các phản hồi. Nó dựa trên phân tích của chúng tôi về các mã trạng thái HTTP mà API REST nên sử dụng và bổ sung các quyết định cấp độ hợp đồng mà hướng dẫn này để ngỏ. Bạn cũng sẽ thấy cách kiểm tra mọi luồng thất bại trong Apidog, bởi vì một hợp đồng lỗi mà bạn không bao giờ kiểm tra thì không phải là một hợp đồng.
Bắt đầu bằng mã trạng thái, không phải nội dung phản hồi
HTTP đã cung cấp cho bạn một lớp ngữ nghĩa lỗi đầu tiên miễn phí. RFC 9110 định nghĩa các nhóm mã trạng thái: 4xx có nghĩa là client đã làm gì đó sai và lặp lại cùng một yêu cầu sẽ tiếp tục thất bại; 5xx có nghĩa là server bị lỗi và yêu cầu của client có thể không có vấn đề gì. Hãy hiểu rõ sự phân chia này trước khi bạn viết một dòng nội dung lỗi nào, bởi vì các client, proxy, bộ nhớ đệm và thư viện thử lại chung đều phân nhánh dựa trên mã trạng thái mà không cần đọc JSON của bạn.
Những lỗi phổ biến nhất thường tập trung quanh một vài cặp mã trạng thái tương tự. Hãy mở tài liệu tham khảo mã trạng thái HTTP của MDN trong khi bạn thiết kế và sử dụng bảng quyết định này cho những mã trạng thái mà các nhóm thường nhầm lẫn.
| Tình huống | Nên dùng | Không nên dùng | Lý do |
|---|---|---|---|
| Yêu cầu bị định dạng sai: JSON hỏng, kiểu nội dung sai, 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 không hiểu yêu cầu |
| Yêu cầu được định dạng đúng nhưng vi phạm quy tắc ngữ nghĩa: số tiền âm, tiền tệ không được hỗ trợ | 422 Unprocessable Content | 400 | Cú pháp ổn; nhưng các giá trị thì không |
| Không có 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 mình là ai. Gửi WWW-Authenticate |
| Thông tin xác thực hợp lệ, quyền hạn không đủ | 403 Forbidden | 401 | Danh tính đã được biết; quyền truy cập bị từ chối. Xác thực lại cũng không có ích |
| Tài nguyên chưa bao giờ tồn tại, hoặc bạn không muốn xác nhận nó tồn tại | 404 Not Found | 410 | Mặc định an toàn; cũng giúp ẩn tài nguyên khỏi các cuộc thăm dò trái phép |
| Tài nguyên đã tồn tại và bị loại bỏ có chủ đích, vĩnh viễn | 410 Gone | 404 | Thông báo cho client và trình thu thập dữ liệu xóa các tham chiếu của họ |
| Xung đột trạng thái: khóa trùng lặp, phiên bản 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 tài nguyên hiện tại |
| Client vượt quá giới hạn tốc độ | 429 Too Many Requests | 503 | Luôn bao gồm Retry-After để client lùi lại đúng cách |
| Ngoại lệ chưa được xử lý trong mã của bạn | 500 Internal Server Error | 502 | Server của bạn bị lỗi |
| Dịch vụ upstream trả về dữ liệu rác cho gateway của bạn | 502 Bad Gateway | 500 | Lỗi xảy ra ở phía sau của cạnh, không phải ở chính nó |
| Server quá tải hoặc đang bảo trì | 503 Service Unavailable | 500 | Tạm thời theo định nghĩa; thêm Retry-After nếu có thể |
| Dịch vụ upstream hết thời gian chờ | 504 Gateway Timeout | 500 | Phân biệt "phụ thuộc chậm" với "mã bị lỗi" |
Hai trong số này đáng được nhấn mạnh thêm. Thứ nhất, 401 so với 403 là một ranh giới bảo mật, không phải là một lựa chọn kiểu dáng: trả về 403 cho một người gọi chưa được xác thực sẽ làm lộ thông tin rằng tài nguyên đó tồn tại. Thứ hai, 429 mà không có Retry-After sẽ khiến client liên tục gửi yêu cầu tới bạn trong các vòng lặp chặt chẽ. Nếu bạn giới hạn tốc độ, và bạn nên làm vậy, hãy kết hợp trạng thái với một tín hiệu lùi lại cụ thể; hướng dẫn của chúng tôi về giới hạn tốc độ API bao gồm các phép toán tiêu đề và các thuật toán đằng sau nó.
Một định dạng nội dung lỗi: RFC 9457 Problem Details
Khi mã trạng thái đã chính xác, mọi lỗi mà API của bạn trả về nên chia sẻ một kiểu phương tiện và một schema. Câu trả lời tiêu chuẩn là RFC 9457 Problem Details, được phục vụ dưới dạng application/problem+json. Nó định nghĩa năm thành phần cốt lõi: type (một URI xác định loại lỗi), title (một tóm tắt ngắn gọn dễ hiểu), status (mã HTTP, được lặp lại để tiện lợi), detail (chi tiết về những gì đã sai trong lần xảy ra này), và instance (một URI cho lỗi cụ thể này). Bất cứ thứ gì khác đều thuộc về các thành phần mở rộng do bạn tự định nghĩa.
Chúng tôi sẽ không dẫn xuất lại đặc tả ở đây; phần giải thích RFC 9457 của chúng tôi sẽ đi sâu vào từng thành phần, các quy tắc đăng ký và cách nó thay thế RFC 7807. Điều quan trọng đối với hợp đồng của bạn là mô hình: cấu trúc chuẩn, các phần mở rộng tùy chỉnh. Dưới đây là một ví dụ về lỗi xác thực trên một endpoint thanh toán.
POST /v1/payments HTTP/1.1
Content-Type: application/json
{ "amount": -1400, "currency": "USD", "source": "card_8xKt2" }
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"
}
Mảng errors[] là một thành phần mở rộng, và nó là thành phần mà các client yêu thích nhất: nó cho phép frontend ánh xạ từng lỗi tới trường biểu mẫu chính xác thay vì hiển thị một thông báo chung chung. Giữ các đường dẫn trường ở định dạng ổn định (JSON Pointer hoặc dotted paths, chọn một) để mã client có thể liên kết chúng theo chương trình.
Một quy tắc sẽ giúp bạn tránh được nhiều rắc rối nhất: trả về định dạng này cho mọi lỗi, bao gồm cả những lỗi mà framework hoặc gateway của bạn tạo ra. Một client nhận được Problem Details từ các handler của bạn nhưng lại nhận HTML từ trang 502 của bộ cân bằng tải vẫn phải viết hai trình phân tích cú pháp.
Mã đọc được bởi máy tính so với thông báo dành cho con người
Hãy lưu ý ví dụ bao gồm cả trường code và message. Đó là một lựa chọn có chủ đích. Chúng phục vụ các đối tượng khác nhau và không bao giờ nên được gộp lại thành một chuỗi duy nhất.
Các mã máy đọc được (AMOUNT_NOT_POSITIVE, CURRENCY_UNSUPPORTED, IDEMPOTENCY_KEY_REUSED) là một phần của hợp đồng. Các client phân nhánh dựa trên chúng, vì vậy chúng phải ổn định, được ghi lại và có thể liệt kê. Không bao giờ buộc client phân tích văn xuôi; ngay khi ai đó viết if (message.includes("positive")), việc chỉnh sửa văn bản của bạn sẽ trở thành một thay đổi gây hỏng.
Thông báo dành cho con người thì ngược lại: có thể cải thiện bất cứ lúc nào, được viết cho nhà phát triển đọc nhật ký và không bao giờ mang tính chịu tải. Hãy nêu rõ lỗi gì đã xảy ra và cách khắc phục nó: “số tiền phải là một số nguyên dương theo đơn vị nhỏ” tốt hơn “số tiền không hợp lệ”. Nếu bạn bản địa hóa, hãy bản địa hóa thông báo và giữ nguyên mã.
Sự phân tách này càng trở nên quan trọng hơn khi người dùng API bao gồm các tác nhân tự động. Các client dựa trên LLM phục hồi tốt hơn nhiều từ các lỗi có cấu trúc, tự mô tả; chúng tôi đề cập đến khía cạnh đó trong thiết kế lỗi API cho các tác nhân AI.
Những gì không bao giờ xuất hiện trong phản hồi lỗi
Các phản hồi lỗi là một kênh trinh sát yêu thích của kẻ tấn công, vì những lỗi chưa được xử lý thường rất dài dòng. Middleware xử lý lỗi của bạn phải đảm bảo rằng không có điều nào sau đây đến được với client:
- Dấu vết stack, tên lớp hoặc đường dẫn tệp
- SQL thô, các đoạn truy vấn hoặc lỗi ORM
- Tên máy chủ nội bộ, IP, cổng hoặc tên dịch vụ
- Phiên bản thư viện và chuỗi banner framework
- Bí mật, token hoặc chuỗi kết nối được nhúng trong văn bản ngoại lệ
- Việc tài khoản người dùng có tồn tại hay không (trong các luồng đăng nhập và đặt lại mật khẩu, hãy giữ các lỗi đối xứng)
Mô hình rất đơn giản: bắt tất cả mọi thứ ở ranh giới, ghi lại toàn bộ ngoại lệ ở phía server với một ID yêu cầu, và trả về một nội dung Problem Details chung với cùng ID đó. Client nhận được "detail": "An internal error occurred", "request_id": "req_51ad0", nhật ký của bạn ghi lại sự thật, và bộ phận hỗ trợ có thể kết nối hai thứ đó lại.
Đánh dấu lỗi có thể thử lại hoặc lỗi cuối cùng
Mỗi lỗi bạn trả về đều trả lời một câu hỏi mà client sắp đặt ra: tôi có nên thử lại lần nữa không? Hãy đưa câu trả lời vào hợp đồng thay vì để mỗi nhóm client phải tự đoán.
Các mã trạng thái mang ngữ nghĩa mặc định. 429, 502, 503 và 504 có thể thử lại với chiến lược lùi lại theo cấp số nhân và độ nhiễu (jitter). 500 thì mơ hồ nhưng thường đáng để thử lại một cách thận trọng. Hầu hết các mã 4xx khác là lỗi cuối cùng (terminal): thử lại 401, 403, 404 hoặc 422 với cùng một yêu cầu sẽ làm lãng phí hạn mức và làm ô nhiễm nhật ký. Hết thời gian chờ cần được quan tâm đặc biệt, vì yêu cầu có thể đã thành công sau khi client bỏ cuộc; đó là vấn đề hết thời gian chờ yêu cầu 408 cổ điển, và đó là lý do tại sao các endpoint thay đổi trạng thái nên chấp nhận khóa đảm bảo tính bất biến (idempotency keys) để một khoản thanh toán được thử lại không thể bị tính phí hai lần.
Bạn cũng có thể làm cho khả năng thử lại trở nên rõ ràng hơn với một thành phần mở rộng:
{
"type": "https://api.example.com/problems/rate-limited",
"title": "Too many requests",
"status": 429,
"code": "RATE_LIMITED",
"retryable": true,
"retry_after_seconds": 30
}
Một cờ retryable rõ ràng cho phép bạn ghi đè các giá trị mặc định khi cần, chẳng hạn như đánh dấu một mã phụ 500 cụ thể là lỗi cuối cùng vì việc thử lại nó làm hỏng trạng thái. Ghi lại cờ này một lần và mọi SDK client bạn phát hành sẽ có hành vi lùi lại thống nhất.
ID tương quan và quản lý phiên bản hợp đồng lỗi
Hai quyết định nhỏ hơn này hoàn thiện hợp đồng, và cả hai đều rẻ bây giờ, đắt sau này.
Gán cho mỗi yêu cầu một ID. Chấp nhận tiêu đề X-Request-Id đến (hoặc tạo một cái), đóng dấu nó trên mọi dòng nhật ký và lặp lại nó trong mọi nội dung lỗi dưới dạng request_id. Khi khách hàng dán một lỗi vào phiếu hỗ trợ, trường đó sẽ biến một giờ tìm kiếm nhật ký thành một truy vấn duy nhất. Trong các thiết lập phân tán, hãy truyền traceparent của W3C cùng với nó để ID theo dõi yêu cầu qua các dịch vụ.
Quản lý phiên bản hợp đồng lỗi của bạn giống như API. Việc thêm một thành phần mở rộng mới hoặc một mã lỗi mới là an toàn. Việc đổi tên errors[].field, thay đổi ý nghĩa của một mã, hoặc chuyển từ một định dạng tùy chỉnh sang Problem Details là một thay đổi gây hỏng (breaking change), và nó làm hỏng các đường dẫn mã mà các nhóm ít kiểm thử nhất. URI type cung cấp cho bạn một cơ chế rõ ràng: giữ các URI loại cũ ổn định vĩnh viễn, giới thiệu các URI mới cho ngữ nghĩa mới và nêu rõ trong tài liệu của bạn rằng các thành phần mở rộng không xác định và các mã không xác định phải được bỏ qua, không được coi là lỗi. Điều khoản tương thích ngược này là điều cho phép bạn phát triển mà không cần phiên bản v2.
Kiểm tra mọi luồng lỗi trong Apidog
Đây là sự thật khó chịu: các hợp đồng lỗi trở nên lỗi thời vì không có gì kiểm tra chúng. Luồng thành công chạy trong mọi bản demo; nhánh 422 chạy khi khách hàng gặp phải. Cách khắc phục là biến các trường hợp thất bại thành công dân hạng nhất trong bộ kiểm thử của bạn, và đây là nơi Apidog khẳng định vị trí của mình trong quy trình làm việc.
Hai tính năng giải quyết trực tiếp vấn đề này.
Các kịch bản kiểm thử cho phía server. Đối với mỗi endpoint, hãy xây dựng một kịch bản cho mỗi trường hợp lỗi: thiếu xác thực mong đợi 401, vai trò không đủ mong đợi 403, số tiền âm mong đợi 422 với errors[0].code bằng AMOUNT_NOT_POSITIVE, lưu lượng truy cập đột biến mong đợi 429 với tiêu đề Retry-After. Các xác nhận trực quan của Apidog kiểm tra trạng thái, tiêu đề và các trường nội dung mà không cần kịch bản, và bạn có thể xác thực toàn bộ payload dựa trên JSON Schema của Problem Details để bất kỳ sự sai lệch nào trong định dạng lỗi sẽ bị lỗi ở CI, chứ không phải ở môi trường sản xuất. Hướng dẫn xác nhận API của chúng tôi trình bày chi tiết các mẫu xác nhận.
Mock server cho phía client. Các nhóm frontend và SDK của bạn cần xây dựng dựa trên các phản hồi 4xx và 5xx trước khi backend có thể tạo ra chúng theo yêu cầu. Các mock server của Apidog trả về chính xác các nội dung Problem Details từ đặc tả API của bạn, vì vậy bạn có thể mô phỏng một 503 với Retry-After: 120, một 409 khi gửi hai lần, hoặc một payload xác thực errors[] đầy đủ, sau đó xem cách client hiển thị và thử lại. Không cần tạo Express stub thủ công, không cần bình luận mã backend để buộc xảy ra lỗi.
Thiết kế hợp đồng lỗi, mã hóa nó thành các kịch bản và mock, sau đó tích hợp cả hai vào CI. Tải Apidog và dùng thử miễn phí; nhập một đặc tả OpenAPI hiện có sẽ giúp bạn có các phản hồi lỗi có thể mock được chỉ trong vài phút.
Tải xuống
Câu hỏi thường gặp
Tôi nên sử dụng 400 hay 422 cho các lỗi xác thực?
Sử dụng 400 khi yêu cầu bị định dạng sai và server không thể hiểu được: JSON không hợp lệ, kiểu nội dung sai, thiếu trường bắt buộc. Sử dụng 422 khi yêu cầu được phân tích cú pháp rõ ràng nhưng các giá trị vi phạm quy tắc nghiệp vụ của bạn, như số tiền thanh toán âm hoặc một loại tiền tệ không được hỗ trợ. Lợi ích thực tế là để chẩn đoán: 422 cho client biết “hãy sửa dữ liệu của bạn,” trong khi 400 nói “hãy sửa định dạng yêu cầu của bạn.” Dù bạn chọn cách phân chia nào, hãy áp dụng nó một cách nhất quán trên mọi endpoint.
application/problem+json là gì?
Đây là kiểu phương tiện được định nghĩa bởi RFC 9457 cho Problem Details, định dạng lỗi JSON tiêu chuẩn cho các API HTTP. Một phản hồi với kiểu nội dung này mang các thành phần type, title, status, detail và instance, cộng với bất kỳ phần mở rộng nào bạn định nghĩa, chẳng hạn như một mảng errors[] cho các lỗi xác thực cấp trường. Việc sử dụng kiểu phương tiện đã đăng ký cho phép các client chung và middleware nhận diện lỗi của bạn mà không cần cấu hình tùy chỉnh. Phần giải thích RFC 9457 của chúng tôi bao gồm toàn bộ đặc tả.
Client nên tự động thử lại những lỗi HTTP nào?
Thử lại các mã 429, 502, 503 và 504 với chiến lược lùi lại theo cấp số nhân (exponential backoff) cộng với độ nhiễu (jitter), tôn trọng tiêu đề Retry-After nếu có. Coi 500 là đáng để thử lại một lần một cách cẩn thận. Không thử lại các phản hồi 4xx khác; yêu cầu sẽ thất bại theo cùng một cách mỗi lần. Đối với các endpoint thay đổi trạng thái, hãy kết hợp việc thử lại với khóa đảm bảo tính bất biến (idempotency keys) để yêu cầu được phát lại không thể tính phí hai lần hoặc tạo hai lần.
Làm cách nào để kiểm tra phản hồi lỗi API mà không làm hỏng backend của tôi?
Mô phỏng chúng. Trỏ client của bạn đến một mock server Apidog trả về chính xác các nội dung 4xx và 5xx từ đặc tả của bạn, sau đó xác minh hành vi hiển thị và thử lại đối với từng loại. Ở phía server, viết các kịch bản kiểm thử gửi các payload không hợp lệ, thiếu xác thực và lưu lượng truy cập đột biến, sau đó xác nhận mã trạng thái, tiêu đề và schema nội dung lỗi. Cả hai phần đều chạy trong CI, vì vậy hợp đồng lỗi vẫn trung thực mà không cần ai phải tự động ép buộc các lỗi.
