8 phút

Mẫu xử lý lỗi API Go để phản hồi rõ ràng, nhất quán

Mẫu xử lý lỗi API Go chuẩn hóa lỗi có kiểu, ánh xạ mã trạng thái HTTP, request ID và thông điệp an toàn mà không làm lộ nội bộ.

Mẫu xử lý lỗi API Go để phản hồi rõ ràng, nhất quán

Tại sao lỗi API không nhất quán làm khách hàng bực bội

Khi mỗi endpoint báo lỗi theo một cách khác nhau, client dần dần mất lòng tin vào API của bạn. Một route trả { "error": "not found" }, route khác trả { "message": "missing" }, và route thứ ba gửi plain text. Dù ý nghĩa có gần giống, mã client giờ phải đoán xem chuyện gì xảy ra.

Hệ quả đến rất nhanh. Các team viết logic phân tích mong manh và thêm trường hợp đặc biệt cho từng endpoint. Retry trở nên rủi ro vì client không thể biết “thử lại sau” khác với “dữ liệu bạn gửi sai”. Ticket hỗ trợ tăng lên vì client chỉ thấy thông điệp mơ hồ, và đội backend của bạn khó mà khớp nó với một dòng log trên server.

Một kịch bản phổ biến: ứng dụng mobile gọi ba endpoint trong quá trình đăng ký. Endpoint đầu trả HTTP 400 với map lỗi theo trường, endpoint hai trả HTTP 500 kèm chuỗi stack trace, và endpoint ba trả HTTP 200 với { "ok": false }. Team app triển khai ba bộ xử lý lỗi khác nhau, và team backend vẫn nhận báo cáo kiểu “đăng ký thỉnh thoảng thất bại” mà không có manh mối rõ ràng để bắt đầu.

Mục tiêu là một hợp đồng dự đoán được. Client nên có thể đọc được chuyện gì đã xảy ra, liệu có phải lỗi do họ hay server, có nên retry hay không, và một request ID để dán vào ticket hỗ trợ.

Phạm vi: bài viết tập trung vào API HTTP trả JSON (không phải gRPC), nhưng cùng ý tưởng áp dụng ở bất kỳ nơi nào bạn trả lỗi cho hệ thống khác.

Một mục tiêu đơn giản: một hợp đồng mà mọi endpoint tuân theo

Chọn một hợp đồng rõ ràng cho lỗi và bắt mọi endpoint tuân thủ. “Nhất quán” có nghĩa là cùng cấu trúc JSON, cùng ý nghĩa các trường, và cùng hành vi dù handler nào gặp lỗi. Khi làm được vậy, client ngừng đoán và bắt đầu xử lý lỗi đúng.

Một hợp đồng hữu dụng giúp client quyết định bước tiếp theo. Với hầu hết ứng dụng, mọi phản hồi lỗi nên trả lời ba câu hỏi:

  • Tôi có thể sửa input không?
  • Có nên thử lại sau không?
  • Tôi có cần liên hệ support không?

Một bộ quy tắc thực tế:

  • Một schema phản hồi cho mọi lỗi.
  • Một chính sách mã trạng thái (cùng loại lỗi luôn ánh xạ sang cùng HTTP status).
  • Một chính sách thông điệp an toàn (cái gì người dùng được thấy vs cái gì là nội bộ).
  • Một móc đối chiếu (một request ID trả về để support tìm lỗi).

Quyết định trước những gì không bao giờ được hiển thị trong phản hồi. Các mục “không bao giờ” phổ biến gồm đoạn SQL, stack trace, hostname nội bộ, secret, và chuỗi lỗi thô từ dependency.

Giữ sự phân tách rõ ràng: một thông điệp ngắn cho người dùng (an toàn, lịch sự, có thể hành động) và chi tiết nội bộ (lỗi đầy đủ, stack, ngữ cảnh) chỉ lưu trong log. Ví dụ: “Không thể lưu thay đổi của bạn. Vui lòng thử lại.” là an toàn. “pq: duplicate key value violates unique constraint users_email_key” thì không.

Khi mọi endpoint tuân theo cùng hợp đồng, client có thể xây dựng một handler lỗi duy nhất và tái sử dụng ở mọi nơi.

Định nghĩa schema phản hồi lỗi mà client có thể dựa vào

Client chỉ xử lý lỗi sạch nếu mọi endpoint trả về cùng một hình dạng. Chọn một phong bì JSON và giữ nó ổn định.

Mặc định thực tế là một đối tượng error cộng với request_id ở cấp cao:

{
  "error": {
    "code": "VALIDATION_FAILED",
    "message": "Some fields are invalid.",
    "details": {
      "fields": {
        "email": "must be a valid email address"
      }
    }
  },
  "request_id": "req_01HV..."
}

HTTP status cho biết hạng mục chung (400, 401, 409, 500). error.code dạng máy cho biết trường hợp cụ thể để client có thể phân nhánh. Sự tách biệt này quan trọng vì nhiều vấn đề khác nhau có thể cùng chung một status. Một app mobile có thể hiển thị UI khác cho EMAIL_TAKEN so với WEAK_PASSWORD, ngay cả khi cả hai đều là 400.

Giữ error.message an toàn và dễ đọc cho con người. Nó nên giúp người dùng sửa lỗi, nhưng không bao giờ tiết lộ nội bộ (SQL, stack trace, tên provider, đường dẫn tập tin).

Các trường tùy chọn hữu ích khi chúng giữ tính dự đoán:

  • Lỗi validation: details.fields là map từ trường tới thông điệp.
  • Giới hạn tốc độ hoặc vấn đề tạm thời: details.retry_after_seconds.
  • Hướng dẫn thêm: details.docs_hint dưới dạng plain text (không phải URL).

Vì backward compatibility, coi giá trị error.code như một phần hợp đồng API. Thêm mã mới mà không thay đổi nghĩa cũ. Chỉ thêm các trường tùy chọn, và giả sử client sẽ bỏ qua trường không nhận ra.

Lỗi có kiểu trong Go: mô hình rõ ràng cho handlers

Xử lý lỗi trở nên lộn xộn khi mỗi handler tự nghĩ ra cách báo thất bại. Một tập nhỏ các lỗi có kiểu khắc phục được điều đó: handler trả các kiểu lỗi đã biết, và một lớp phản hồi chung biến chúng thành phản hồi nhất quán.

Một bộ khởi đầu thực tế bao phủ hầu hết endpoint:

  • ValidationError (input sai)
  • NotFoundError (resource không tồn tại)
  • ConflictError (ràng buộc unique, trạng thái không khớp)
  • UnauthorizedError (chưa đăng nhập hoặc không có quyền)
  • InternalError (mọi thứ khác)

Chìa khóa là ổn định ở mức trên cùng, dù nguyên nhân gốc thay đổi. Bạn có thể bọc lỗi cấp thấp hơn (SQL, mạng, parse JSON) trong khi vẫn trả cùng loại công khai mà middleware có thể phát hiện.

type NotFoundError struct {
	Resource string
	ID       string
	Err      error // private cause
}

func (e NotFoundError) Error() string { return "not found" }
func (e NotFoundError) Unwrap() error { return e.Err }

Trong handler, trả NotFoundError{Resource: "user", ID: id, Err: err} thay vì để lộ sql.ErrNoRows trực tiếp.

Để kiểm tra lỗi, ưu tiên errors.As cho các kiểu tùy chỉnh và errors.Is cho các sentinel error. Sentinel errors (như var ErrUnauthorized = errors.New("unauthorized")) phù hợp cho trường hợp đơn giản, nhưng kiểu tùy chỉnh thắng thế khi bạn cần ngữ cảnh an toàn (ví dụ resource nào bị thiếu) mà không thay đổi hợp đồng phản hồi công khai.

Cần nghiêm ngặt về những gì đính kèm:

  • Công khai (an toàn cho client): thông điệp ngắn, mã ổn định, và đôi khi tên trường cho validation.
  • Nội bộ (chỉ log): Err gốc, thông tin stack, lỗi SQL thô, token, dữ liệu người dùng.

Sự phân tách đó cho phép bạn giúp client mà không lộ nội bộ.

Ánh xạ loại lỗi sang mã trạng thái HTTP một cách nhất quán

Khi đã có lỗi có kiểu, công việc tiếp theo là buồn chán nhưng cần thiết: cùng một kiểu lỗi luôn phải tạo ra cùng một HTTP status. Client sẽ dựa vào điều đó để xây dựng logic.

Một ánh xạ thực tế cho hầu hết API:

Error type (ví dụ)StatusKhi nào dùng
BadRequest (JSON sai, thiếu query param)400Request không hợp lệ ở mức giao thức hoặc định dạng.
Unauthenticated (không có/ token không hợp lệ)401Client cần xác thực.
Forbidden (không có quyền)403Auth hợp lệ nhưng không được phép truy cập.
NotFound (ID resource không tồn tại)404Resource yêu cầu không có (hoặc bạn chọn che tồn tại).
Conflict (unique constraint, version mismatch)409Request đúng định dạng nhưng xung đột với trạng thái hiện tại.
ValidationFailed (quy tắc trường)422Hình dạng đúng nhưng validation nghiệp vụ thất bại (email format, min length).
RateLimited429Quá nhiều request trong cửa sổ thời gian.
Internal (lỗi không mong đợi)500Bug hoặc lỗi không mong đợi.
Unavailable (dependency down, timeout, bảo trì)503Vấn đề phía server tạm thời.

Hai phân biệt giúp tránh nhiều nhầm lẫn:

  • 400 vs 422: dùng 400 khi bạn không thể giải thích request (JSON sai, kiểu sai). Dùng 422 khi bạn parse được, nhưng giá trị không chấp nhận được.
  • 409 vs 422: dùng 422 cho validation ở mức trường (mật khẩu quá ngắn). Dùng 409 khi dữ liệu hợp lệ nhưng không thể áp dụng do trạng thái (email đã tồn tại, order đã shipped, optimistic lock thất bại).

Hướng dẫn retry quan trọng:

  • Thường an toàn để retry: 503, và đôi khi 429 (sau khi chờ).
  • Thường không an toàn để retry mà không thay đổi: 400, 401, 403, 404, 409, 422.
  • Nếu thao tác idempotent (PUT cùng body, hoặc POST với idempotency key), retry an toàn hơn ngay cả khi lỗi tạm thời.

Request ID: cách nhanh nhất để debug vấn đề client

Xây dựng API Go nhanh hơn
Mô tả API Go của bạn và tạo handlers theo một mẫu phản hồi lỗi duy nhất.

Request ID là một giá trị ngắn, duy nhất nhận diện một cuộc gọi API end-to-end. Nếu client thấy nó trong mọi phản hồi, support trở nên đơn giản: “Gửi request ID cho tôi” thường là đủ để tìm đúng log và nguyên nhân.

Thói quen này có lợi cho cả phản hồi thành công lẫn lỗi.

Quy tắc tạo và truyền

Dùng một quy tắc rõ ràng: nếu client gửi request ID, giữ nguyên. Nếu không, tạo mới.

  • Chấp nhận ID đến từ một header đơn (chọn một và ghi tài liệu, ví dụ X-Request-Id).
  • Nếu header thiếu hoặc rỗng, tạo ID mới ở edge (middleware) và đính vào context của request.
  • Không bao giờ thay đổi ID giữa chừng. Truyền qua các cuộc gọi xuống (DB, service khác) bằng context hoặc header.

Đặt request ID ở ba nơi:

  • Header phản hồi (cùng tên header bạn chấp nhận)
  • Body phản hồi (như request_id trong schema chuẩn)
  • Log (như một trường có cấu trúc trên mọi dòng log)

Batch và công việc bất đồng bộ

Với endpoint batch hoặc job nền, giữ một parent request ID. Ví dụ: client upload 200 dòng, 12 dòng fail validation, và bạn enqueue công việc. Trả một request_id cho toàn bộ gọi, và bao gồm parent_request_id trên mỗi job và mỗi lỗi theo mục. Bằng vậy bạn có thể truy vết “một upload” ngay cả khi nó tỏa ra nhiều tác vụ.

Logging và metrics mà không làm lộ nội bộ

Client cần phản hồi lỗi rõ ràng, ổn định. Log của bạn cần sự thật lộn xộn. Tách hai thế giới này: trả thông điệp an toàn và mã lỗi công khai cho client, trong khi ghi nguyên nhân nội bộ, stack, và ngữ cảnh trong log server.

Ghi một event có cấu trúc cho mỗi phản hồi lỗi, có thể tìm bằng request_id.

Các trường nên duy trì nhất quán:

  • request_id
  • user_id hoặc account_id (khi đã xác thực)
  • mã lỗi công khai và HTTP status
  • tên handler/route và method
  • chi tiết lỗi nội bộ (nguyên nhân bọc, lỗi validation theo trường, timeout upstream)

Lưu chi tiết nội bộ chỉ trong server logs (hoặc kho lỗi nội bộ). Client không bao giờ nên thấy lỗi DB thô, câu truy vấn, stack trace, hay thông điệp nhà cung cấp. Nếu bạn chạy nhiều service, một trường nội bộ như source (api, db, auth, upstream) có thể giúp phân loại nhanh.

Theo dõi các endpoint ồn ào và lỗi bị rate-limit. Nếu một endpoint có thể sinh ra cùng 429 hoặc 400 hàng nghìn lần/phút, tránh spam log: sample các event lặp lại, hoặc hạ mức severity cho lỗi dự kiến trong khi vẫn đếm chúng trong metrics.

Metrics bắt bệnh sớm hơn logs. Theo dõi số lượng theo nhóm HTTP status và error code, và cảnh báo khi có spike đột ngột. Nếu RATE_LIMITED tăng 10x sau deploy, bạn sẽ thấy nhanh ngay cả khi logs bị sample.

Từng bước: triển khai pipeline lỗi nhất quán trong Go

Thay đổi an toàn khi refactor
Lặp nhanh với snapshot và rollback khi bạn tinh chỉnh schema lỗi.

Cách dễ nhất để làm cho lỗi nhất quán là ngừng xử lý chúng “mọi nơi” và dẫn chúng qua một pipeline nhỏ. Pipeline này quyết định client thấy gì và bạn lưu gì cho logs.

Pipeline trong 5 bước thực tế

Bắt đầu với một tập nhỏ mã lỗi mà client có thể phụ thuộc (ví dụ: INVALID_ARGUMENT, NOT_FOUND, UNAUTHORIZED, CONFLICT, INTERNAL). Bọc chúng trong một lỗi có kiểu mà chỉ phơi bày các trường công khai an toàn (code, thông điệp an toàn, details tùy chọn như trường nào sai). Giữ nguyên nhân nội bộ riêng.

Rồi triển khai một hàm dịch duy nhất chuyển bất kỳ lỗi nào thành (statusCode, responseBody). Tại đây các lỗi có kiểu ánh xạ sang HTTP status, còn lỗi chưa biết thì trở thành 500 an toàn.

Tiếp theo, thêm middleware mà:

  • đảm bảo mỗi request có request_id
  • recover từ panic

Panic không bao giờ nên đổ stack trace ra client. Trả 500 bình thường với thông điệp chung, và ghi log đầy đủ panic với cùng request_id.

Cuối cùng, thay đổi handlers để chúng trả error thay vì tự viết response. Một wrapper gọi handler, chạy translator, và viết JSON theo dạng chuẩn.

Một checklist ngắn gọn:

  • Định nghĩa lỗi có kiểu với các trường an toàn và mã ổn định.
  • Dịch lỗi sang status và JSON phản hồi ở một nơi duy nhất.
  • Thêm middleware request ID và recover panic.
  • Làm handlers trả lỗi, không trả response trực tiếp.
  • Thêm golden tests cho translator và wrapper.

Golden tests quan trọng vì chúng khóa hợp đồng. Nếu ai đó sau này thay đổi thông điệp hoặc mã trạng thái, test sẽ fail trước khi client bị bất ngờ.

Ví dụ: một endpoint, ba lỗi, phản hồi dự đoán được

Giả sử một endpoint: app client tạo bản ghi customer.

POST /v1/customers với JSON như { "email": "[email protected]", "name": "Pat" }. Server luôn trả cùng dạng lỗi và luôn có request_id.

1) Lỗi validation (400)

Email bị thiếu hoặc sai định dạng. Client có thể gắn highlight lên trường.

{
  "request_id": "req_01HV9N2K6Q7A3W1J9K8B",
  "error": {
    "code": "VALIDATION_FAILED",
    "message": "Some fields need attention.",
    "details": {
      "fields": {
        "email": "must be a valid email address"
      }
    }
  }
}

2) Conflict (409)

Email đã tồn tại. Client có thể gợi ý đăng nhập hoặc chọn email khác.

{
  "request_id": "req_01HV9N3C2D0F0M3Q7Z9R",
  "error": {
    "code": "ALREADY_EXISTS",
    "message": "A customer with this email already exists."
  }
}

3) Lỗi tạm thời (503)

Một dependency bị down. Client có thể retry với backoff và hiển thị thông điệp nhẹ nhàng.

{
  "request_id": "req_01HV9N3X8P2J7T4N6C1D",
  "error": {
    "code": "TEMPORARILY_UNAVAILABLE",
    "message": "We could not save your request right now. Please try again."
  }
}

Với một hợp đồng duy nhất, client phản ứng nhất quán:

  • 400: đánh dấu trường bằng details.fields
  • 409: hướng người dùng tới bước tiếp theo an toàn
  • 503: gợi ý retry và hiển thị request_id như mã hỗ trợ

Với support, cùng request_id đó là con đường nhanh nhất đến nguyên nhân thực trong logs, mà không phơi bày stack trace hay lỗi DB.

Bẫy phổ biến làm xử lý lỗi tệ hơn

Cách nhanh nhất để làm khách hàng bực là bắt họ đoán. Nếu một endpoint trả { "error": "..." } và endpoint khác trả { "message": "..." }, mọi client biến thành đống trường hợp đặc biệt, và bug ẩn trong nhiều tuần.

Một vài sai lầm thường gặp:

  • Trả HTTP 200 kèm lỗi trong body, hoặc chuyển giữa nhiều schema lỗi khác nhau.
  • Phơi bày nội bộ trong thông điệp người dùng, như lỗi SQL, stack trace, IP, hostname dependency, hoặc đường dẫn file.
  • Dùng văn bản tiếng người làm định danh duy nhất thay vì mã code ổn định để client bám vào.
  • Thay đổi mã lỗi một cách tùy tiện (hoặc dùng lại cùng mã cho vấn đề khác) và phá vỡ client viết theo hành vi cũ.
  • Chỉ thêm request_id khi thất bại, khiến bạn không thể đối chiếu báo cáo người dùng với cuộc gọi thành công trước đó.

Lộ nội bộ là cái bẫy dễ rơi vào nhất. Một handler trả err.Error() vì tiện, rồi một tên constraint hay thông điệp bên thứ ba lọt vào phản hồi production. Giữ thông điệp client ngắn gọn và an toàn, và đặt nguyên nhân chi tiết vào logs.

Dựa vào văn bản thuần (text) là một vấn đề kéo dài. Nếu client phải parse câu tiếng Anh như “email already exists”, bạn không thể thay đổi câu chữ mà không phá vỡ logic. Mã lỗi ổn định cho phép bạn điều chỉnh thông điệp, dịch chúng, và giữ hành vi nhất quán.

Xem mã lỗi như một phần hợp đồng công khai. Nếu phải thay đổi, thêm mã mới và giữ mã cũ hoạt động một thời gian, ngay cả khi cả hai cùng ánh xạ sang cùng một HTTP status.

Cuối cùng, bao gồm cùng một trường request_id trong mọi phản hồi, thành công hay thất bại. Khi người dùng nói “nó từng hoạt động, rồi bị lỗi”, một ID thường cứu được cả giờ đồng hồ dò tìm.

Checklist nhanh trước khi phát hành

Từ mã tới triển khai
Triển khai dịch vụ và giữ phản hồi lỗi nhất quán giữa các môi trường.

Trước khi release, kiểm tra nhanh sự nhất quán:

  • Một dạng lỗi cho mọi nơi. Mọi endpoint trả cùng các trường JSON (ví dụ: error.code, error.message, request_id).
  • Mã lỗi ổn định và có bao phủ. Giữ mã ngắn gọn (VALIDATION_FAILED, NOT_FOUND, CONFLICT, UNAUTHORIZED). Thêm test để handlers không trả mã không xác định.
  • Một quy tắc ánh xạ status duy nhất. Quy định cách mỗi loại lỗi ánh xạ sang HTTP status và áp dụng ở một chỗ chung.
  • Request ID hai chiều. Luôn trả request_id và log nó cho mọi request, bao gồm panic và timeout.
  • Thông điệp an toàn theo mặc định. Thông điệp hiển thị cho người dùng nên ngắn, rõ ràng, có thể hành động, không bao giờ là stack trace, lỗi SQL, hay tên vendor.

Sau đó, kiểm tra vài endpoint thủ công. Kích hoạt lỗi validation, record không tồn tại, và lỗi bất ngờ. Nếu phản hồi khác nhau giữa các endpoint (các trường thay đổi, mã trạng thái lệch, thông điệp tiết lộ quá nhiều), sửa pipeline chung trước khi thêm tính năng.

Quy tắc thực tế: nếu một thông điệp có thể giúp attacker hoặc làm người dùng bối rối, nó thuộc về logs chứ không phải phản hồi.

Bước tiếp theo: chuẩn hóa ngay và giữ nhất quán về sau

Viết ra hợp đồng lỗi bạn muốn mọi endpoint tuân theo, ngay cả khi API đã live. Một hợp đồng chung (status, mã lỗi ổn định, thông điệp an toàn, và request_id) là cách nhanh nhất để làm lỗi dễ dự đoán cho client.

Rồi di chuyển dần. Giữ handlers hiện tại, nhưng dẫn lỗi của chúng qua một mapper chung biến lỗi nội bộ thành dạng phản hồi công khai của bạn. Cách này cải thiện tính nhất quán mà không phải rewrite lớn mạo hiểm, và ngăn các endpoint mới tự phát minh định dạng.

Giữ một catalog mã lỗi nhỏ và coi nó là một phần API. Khi ai đó muốn thêm mã mới, rà soát nhanh: nó thực sự mới không, tên có rõ ràng không, và nó ánh xạ tới HTTP status đúng không?

Thêm vài test bắt drift:

  • Mọi phản hồi lỗi có request_id.
  • Mã trạng thái khớp loại lỗi (không phải khớp theo text lỗi).
  • error.code tồn tại và thuộc catalog.
  • error.message an toàn và không bao gồm chi tiết nội bộ.
  • Lỗi không biết rơi về 500 với thông điệp chung.

Nếu bạn xây backend Go từ đầu, khóa hợp đồng sớm sẽ hữu ích. Ví dụ, Koder.ai (koder.ai) có chế độ planning cho phép định nghĩa các quy ước như schema lỗi và catalog mã lỗi trước, rồi giữ handlers thẳng hàng khi API phát triển.

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

What should a “consistent error response” look like?

Sử dụng một dạng JSON duy nhất cho mọi phản hồi lỗi trên tất cả các endpoint. Một mẫu mặc định hữu dụng là request_id ở cấp cao nhất cùng một đối tượng error gồm code, message, và details tùy chọn để client có thể phân tích và xử lý một cách tin cậy.

How do I avoid leaking internal details in API errors?

Trả error.message dưới dạng câu ngắn, an toàn cho người dùng và lưu nguyên nhân thực sự trong log máy chủ. Không trả lỗi cơ sở dữ liệu thô, stack trace, hostname nội bộ hay thông điệp từ nhà cung cấp, ngay cả khi điều đó có vẻ hữu ích khi đang phát triển.

Do I really need an error code if I already have HTTP status codes?

Dùng một error.code ổn định cho logic máy và để HTTP status mô tả hạng mục chung. Client nên phân nhánh theo error.code (ví dụ ALREADY_EXISTS) và coi status như hướng dẫn (ví dụ 409 nghĩa là xung đột trạng thái).

When should I use HTTP 400 vs 422?

Dùng 400 khi request không thể phân tích hoặc giải thích đáng tin cậy (JSON sai, kiểu dữ liệu sai). Dùng 422 khi request có thể parse nhưng vi phạm quy tắc nghiệp vụ (email không hợp lệ, mật khẩu quá ngắn).

When should I use HTTP 409 vs 422?

Dùng 409 khi input hợp lệ nhưng không thể áp dụng vì xung đột với trạng thái hiện tại (email đã tồn tại, version mismatch). Dùng 422 cho validation ở mức trường dữ liệu, nơi thay đổi giá trị có thể khắc phục mà không cần thay đổi trạng thái server.

How do typed errors in Go help keep responses consistent?

Tạo một bộ lỗi có kiểu nhỏ (validation, not found, conflict, unauthorized, internal) và để handlers trả chúng. Sau đó dùng một bộ dịch chung để ánh xạ các kiểu đó sang status HTTP và mẫu JSON chuẩn.

How should I generate and return request IDs?

Luôn trả request_id trong mọi phản hồi (thành công hay lỗi) và ghi nó trên mọi dòng log. Khi client báo lỗi, một request_id thường là đủ để tìm chính xác luồng thực thi và nguyên nhân trong log.

Why is returning HTTP 200 with `{ "ok": false }` a bad idea?

Chỉ trả 200 khi thao tác thành công; dùng 4xx/5xx cho lỗi. Giấu lỗi sau 200 khiến client phải parse nội dung body và tạo hành vi không nhất quán giữa các endpoint.

Which errors should clients retry, and which should they not?

Không retry cho mặc định với 400, 401, 403, 404, 409, 422 vì retry thường không giúp nếu không thay đổi gì. Cho phép retry với 503, và đôi khi 429 sau khi chờ; nếu hỗ trợ idempotency keys thì retry an toàn hơn cho POST khi lỗi tạm thời.

How do I prevent error responses from drifting as the API evolves?

Khóa hợp đồng bằng vài bài test “golden” khẳng định status, error.code, và sự hiện diện của request_id. Thêm mã lỗi mới mà không phá vỡ nghĩa cũ, và chỉ thêm các trường tùy chọn để client cũ vẫn hoạt động.

Related posts