Lỗi
Nếu bạn đã dành nhiều thời gian xây dựng web server bằng Go, có lẽ bạn đã từng thấy thông báo khó chịu này xuất hiện trong console:
2023/10/27 10:00:00 http: multiple response.WriteHeader calls
Server của bạn không bị sập, nó vẫn tiếp tục chạy. Tuy nhiên, log này là một cảnh báo rằng request handler của bạn đang cố gắng làm một điều không thể: thay đổi quá khứ. Trong Go, một khi bạn đã gửi mã trạng thái (status code) HTTP, nó sẽ không thể thay đổi đối với request cụ thể đó.
Nguyên nhân gốc rễ
Trong package net/http, http.ResponseWriter là đường một chiều. Một khi các header đã được gửi đi, chúng sẽ biến mất. Lỗi này thường xuất phát từ việc rò rỉ logic khi code của bạn cố gắng thiết lập mã trạng thái hoặc ghi vào response body nhiều lần.
1. "Quên lệnh Return"
Đây là nguyên nhân trong 90% trường hợp. Bạn phát hiện một lỗi, gọi http.Error(), nhưng sau đó lại để hàm tiếp tục chạy. http.Error thực chất có gọi WriteHeader bên dưới. Nếu code của bạn gặp một lệnh WriteHeader hoặc Write khác sau đó, Go sẽ kích hoạt cảnh báo.
2. Header ngầm định và tường minh
Go luôn cố gắng hỗ trợ người dùng. Nếu bạn gọi w.Write() mà không gọi w.WriteHeader() trước, Go sẽ giả định bạn muốn gửi 200 OK và gửi header đó ngay lập tức. Nếu bạn cố gắng thiết lập một mã trạng thái khác sau khi ghi dữ liệu, bạn đã quá muộn.
Cách khắc phục
Kịch bản A: Thiếu lệnh Return
Hãy xem xét sai lầm phổ biến này. Nếu thiếu id, code sẽ gửi lỗi 400 nhưng không dừng lại. Nó tiếp tục chạy xuống logic 200 OK.
func handler(w http.ResponseWriter, r *http.Request) {
id := r.URL.Query().Get("id")
if id == "" {
http.Error(w, "Thiếu ID", http.StatusBadRequest)
// Việc thực thi vẫn tiếp tục ở đây!
}
w.WriteHeader(http.StatusOK)
w.Write([]byte("Thành công"))
}
Cách sửa: Sử dụng guard clause. Luôn luôn return ngay sau khi gửi phản hồi lỗi để dừng việc thực thi hàm tại đó.
func handler(w http.ResponseWriter, r *http.Request) {
id := r.URL.Query().Get("id")
if id == "" {
http.Error(w, "Thiếu ID", http.StatusBadRequest)
return // Đúng: Hàm thoát tại đây
}
w.WriteHeader(http.StatusOK)
w.Write([]byte("Thành công"))
}
Kịch bản B: Thứ tự các lệnh gọi
Bạn không thể thay đổi mã trạng thái một khi dữ liệu đã được gửi đi. Đoạn code này thất bại vì 200 OK được gửi ngay khi chuỗi "Đang xử lý..." được ghi.
func handler(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("Đang xử lý..."))
// Lệnh này kích hoạt lỗi vì header đã được gửi đi rồi
w.WriteHeader(http.StatusCreated)
}
Cách sửa: Thiết lập mã trạng thái và header của bạn trước khi ghi bất kỳ byte nào vào body. Nếu bạn đang sử dụng json.NewEncoder().Encode(), hãy thiết lập các header trước.
func handler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusCreated)
json.NewEncoder(w).Encode(map[string]string{"status": "hoàn thành"})
}
Kịch bản C: Middleware bị lặp phản hồi
Middleware thường bao bọc lệnh gọi next.ServeHTTP(w, r) call. Nếu middleware của bạn ghi vào response sau khi gọi handler tiếp theo, bạn có thể xung đột với những gì handler bên trong đã gửi.
Cách sửa: Nếu bạn cần sửa đổi response sau khi handler chạy, bạn phải sử dụng một wrapper ResponseWriter tùy chỉnh để đệm (buffer) đầu ra hoặc ghi lại mã trạng thái trước khi nó được gửi đến client.
Xác minh
Làm thế nào để biết lỗi đã thực sự được sửa? Đừng chỉ đoán. Hãy thực hiện các kiểm tra sau:
- **Theo dõi Log:** Thực hiện hành động đã kích hoạt lỗi. Nếu console không hiện cảnh báo, có vẻ bạn đã thành công.
- **Kiểm tra với Curl:** Sử dụng `curl -v http://localhost:8080/endpoint`. Tìm dòng `< HTTP/1.1 200 OK`. Đảm bảo bạn chỉ thấy đúng một dòng trạng thái và không có nội dung body ngoài ý muốn.
- **Kiểm tra với NewRecorder:** Sử dụng package `httptest` trong unit test. Nó cho phép bạn kiểm tra `Result().StatusCode` và đảm bảo logic của bạn chạy đúng qua các nhánh điều kiện.
Thực hành tốt nhất
- **Return Early:** Áp dụng mô hình "Return Early" (trả về sớm). Nó giúp code của bạn gọn gàng và ngăn chặn việc thực thi nhầm các logic phụ phía sau.
- **Điểm phản hồi duy nhất:** Cố gắng chỉ có một nơi duy nhất trong handler thực hiện việc ghi phản hồi thành công cuối cùng.
- **Lint Code:** Sử dụng `golangci-lint`. Nó có thể phát hiện một số trường hợp mà luồng thực thi có thể dẫn đến việc ghi phản hồi nhiều lần.
- **Thứ tự Header:** Luôn tuân theo trình tự: 1. Thiết lập Header, 2. WriteHeader, 3. Ghi Body.

