Điều gì đang xảy ra?
Hãy tưởng tượng bạn đang xây dựng một bộ lọc đăng nhập (login filter) hoặc kiểm tra quyền hạn trong Spring Boot. Mọi thứ trông có vẻ hoàn hảo, nhưng rồi console của bạn bùng nổ với lỗi java.lang.IllegalStateException. Thông báo rất rõ ràng: bạn đã cố gắng chuyển hướng người dùng, nhưng phản hồi (response) đã được "committed" (gửi đi) rồi.
Hãy coi một HTTP response giống như một bức thư tay. Một khi bạn đã bỏ nó vào hòm thư (buffer đã flush), bạn không thể thò tay vào để thay đổi địa chỉ hoặc dán thêm tem mới. Trong thuật ngữ Java Web, một khi máy chủ gửi các HTTP header hoặc khối dữ liệu đầu tiên đến client, trạng thái phản hồi sẽ bị khóa. Bạn không còn có thể thay đổi mã trạng thái (status code) hoặc kích hoạt lệnh chuyển hướng (redirect) được nữa.
Tại sao điều này xảy ra
Vòng đời của HTTP rất nghiêm ngặt. Dữ liệu chỉ chảy theo một hướng. Thông thường, lỗi này bắt nguồn từ một trong bốn sai sót kỹ thuật sau:
- Thực thi "ma" (Ghost Execution): Bạn đã gọi
sendRedirect(), nhưng code vẫn tiếp tục chạy và cố gắng ghi thêm dữ liệu ở phía dưới phương thức. - Tràn bộ đệm (Buffer Overflow): Hầu hết các máy chủ, như Apache Tomcat, có kích thước bộ đệm mặc định là 8.192 bytes (8KB). Nếu code của bạn ghi 9KB dữ liệu, máy chủ sẽ tự động flush bộ đệm và commit phản hồi.
- Sử dụng Filter chồng chéo: Một bộ lọc bảo mật xử lý lỗi và chuyển hướng người dùng, nhưng sau đó lại vô tình gọi
chain.doFilter(), chuyển yêu cầu sang thành phần tiếp theo. - Flush thủ công: Bạn đã gọi
response.flushBuffer()hoặc đóng mộtPrintWriterquá sớm.
Cách khắc phục
1. Câu lệnh 'return' quan trọng
Đây là sai lầm phổ biến nhất. Nhiều lập trình viên cho rằng sendRedirect() hoạt động giống như một câu lệnh return. Thực tế không phải vậy. Nó chỉ đơn giản là thiết lập một header. Phần còn lại của phương thức vẫn sẽ thực thi trừ khi bạn dừng nó lại một cách thủ công.
Cách làm sai:
public void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException {
if (session.getAttribute("user") == null) {
response.sendRedirect("/login");
}
// Đoạn này vẫn chạy! Nó sẽ cố gắng ghi vào một response đã commit.
response.getWriter().write("Welcome to the dashboard");
}
Cách làm đúng:
public void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException {
if (session.getAttribute("user") == null) {
response.sendRedirect("/login");
return; // Thoát khỏi phương thức ngay lập tức
}
response.getWriter().write("Welcome to the dashboard");
}
2. Dọn dẹp logic trong Filter
Nếu bạn đang sử dụng một Filter để xác thực, hãy đảm bảo bạn không chuyển yêu cầu xuống chuỗi (chain) sau khi đã xử lý xong. Một khi bạn gửi lỗi hoặc chuyển hướng, chuỗi phải kết thúc tại đó.
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) {
HttpServletResponse res = (HttpServletResponse) response;
if (isTokenInvalid(request)) {
res.sendRedirect("/login");
return; // Dừng lại ở đây! Không gọi chain.doFilter()
}
chain.doFilter(request, response);
}
3. Sử dụng isCommitted() để kiểm tra an toàn
Trong các ứng dụng phức tạp, nơi nhiều tiện ích có thể tác động đến phản hồi, hãy sử dụng phương thức isCommitted(). Đây là một kiểm tra boolean đơn giản giúp ngăn ứng dụng của bạn bị crash khi cố gắng sửa đổi một phản hồi đã hoàn tất.
if (!response.isCommitted()) {
response.sendRedirect(targetUrl);
} else {
log.warn("Cannot redirect to {} because the response is already out the door.", targetUrl);
}
4. Spring Boot và @ControllerAdvice
Hãy cẩn thận khi kết hợp việc ghi HttpServletResponse thủ công với các bộ xử lý ngoại lệ toàn cục (global exception handlers). Nếu bạn ghi một phần phản hồi và sau đó ném ra một ngoại lệ, BasicErrorController của Spring sẽ cố gắng gửi một trang lỗi 500. Vì bạn đã bắt đầu ghi phản hồi đầu tiên, sự xung đột này sẽ kích hoạt IllegalStateException.
Cách xác minh bản sửa lỗi
- Theo dõi Tab Network: Mở Browser DevTools (F12). Nếu bạn thấy mã 302 Found được theo sau ngay lập tức bởi mã 500 Internal Server Error cho cùng một URL, nghĩa là bạn đang gặp lỗi logic.
- Truy vết Stack Trace: Stack trace thường chỉ vào lần thử sửa đổi phản hồi thứ hai. Hãy nhìn lên khoảng 10-20 dòng phía trên trong code của bạn để tìm lần sửa đổi đầu tiên thực sự đã commit phản hồi đó.
- Kiểm tra việc sử dụng Buffer: Nếu lỗi xảy ra trên các trang lớn, hãy kiểm tra xem bạn có đang gọi
out.flush()bên trong một vòng lặp hay không.
Tổng kết
Lỗi Response already committed về cơ bản là một lỗi vi phạm giao thông trong giao thức HTTP. Để khắc phục, hãy đảm bảo rằng một khi bạn yêu cầu máy chủ chuyển hướng hoặc kết thúc một phản hồi, bạn phải dừng mọi hoạt động thực thi tiếp theo trong luồng yêu cầu đó. Một lệnh return; đơn giản thường là tất cả những gì bạn cần.

