Sửa lỗi Nginx WebSocket 400: Hướng dẫn thực tế về Reverse Proxy

intermediate🌐 Networking2026-06-18| Ubuntu/Debian/CentOS, Nginx 1.10+, Backend: Node.js (Socket.io), Go, hoặc Python (FastAPI/Django Channels)

Error Message

WebSocket connection to 'wss://example.com/ws' failed: Error during WebSocket handshake: Unexpected response code: 400
#websocket#nginx#proxy#wss#socket.io

Vấn đề

Mọi thứ hoạt động hoàn hảo trên môi trường local của bạn. Nhưng ngay khi bạn triển khai phía sau một Nginx reverse proxy, các tính năng thời gian thực (real-time) bị hỏng. Thay vì bắt tay thành công, trình duyệt sẽ hiển thị một lỗi gây khó chịu:

WebSocket connection to 'wss://example.com/ws' failed: Error during WebSocket handshake: Unexpected response code: 400

Đừng quá lo lắng. Nguyên nhân thường nằm ở hành vi mặc định của Nginx. Nó coi mọi yêu cầu gửi đến là traffic HTTP tiêu chuẩn. Khi làm vậy, Nginx sẽ loại bỏ các header cụ thể mà backend cần để duy trì kết nối liên tục.

Giải pháp nhanh

Bạn cần chỉ định Nginx truyền các header UpgradeConnection trực tiếp đến backend. Hãy mở tệp cấu hình site của bạn—thường nằm ở /etc/nginx/sites-available/—và cập nhật block location:

location /ws {
    proxy_pass http://localhost:3000;
    proxy_http_version 1.1;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection "upgrade";
    proxy_set_header Host $host;

    # Ngăn timeout mặc định 60 giây ngắt các kết nối đang chờ
    proxy_read_timeout 86400;
}

Kiểm tra cú pháp và tải lại dịch vụ để áp dụng các thay đổi:

sudo nginx -t
sudo systemctl reload nginx

Tại sao lỗi này xảy ra (Phân tích kỹ thuật)

WebSocket rất đặc biệt. Chúng bắt đầu như một yêu cầu HTTP tiêu chuẩn và sau đó "nâng cấp" lên một giao thức bền vững bằng cách sử dụng hai header cụ thể: Upgrade: websocketConnection: Upgrade.

Nginx tuân thủ nghiêm ngặt đặc tả HTTP/1.1. Nó coi đây là các header "hop-by-hop", nghĩa là nó sẽ không truyền chúng từ client đến server proxy trừ khi bạn yêu cầu rõ ràng. Khi backend Node.js hoặc Go nhận được yêu cầu WebSocket mà thiếu các header này, nó sẽ coi đó là yêu cầu sai định dạng. Kết quả là lỗi HTTP 400 Bad Request mà bạn đang gặp phải.

Xác định nguyên nhân gốc rễ

Lỗi 400 là tín hiệu của việc không khớp giao thức. Nếu log backend của bạn hiển thị một nỗ lực kết nối nhưng biến mất ngay lập tức, khả năng cao Nginx đã chặn các header bắt tay trước khi chúng kịp đến được code ứng dụng của bạn.

Cấu hình nâng cao cho môi trường Production

Việc fix cứng "upgrade" trong block location là ổn cho các trường hợp đơn lẻ. Tuy nhiên, nếu server của bạn xử lý cả HTTP thông thường và WebSocket, việc ánh xạ động sẽ chuyên nghiệp hơn nhiều. Điều này đảm bảo các yêu cầu tiêu chuẩn vẫn giữ trạng thái keep-alive trong khi WebSocket nhận được sự nâng cấp cần thiết.

Thêm đoạn ánh xạ này bên trong block http của tệp /etc/nginx/nginx.conf:

map $http_upgrade $connection_upgrade {
    default upgrade;
    ''      close;
}

Giờ đây, bạn có thể sử dụng cấu hình linh hoạt hơn cho tất cả các site của mình:

location /ws {
    proxy_pass http://backend_cluster;
    proxy_http_version 1.1;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection $connection_upgrade;
}

Các bước xác minh

Kiểm tra ba điều sau để xác nhận bản sửa lỗi của bạn thực sự có hiệu lực:

- **Kiểm tra tab Network:** Mở DevTools (F12) và lọc theo "WS". Yêu cầu `/ws` của bạn bây giờ sẽ hiển thị trạng thái `101 Switching Protocols`.
- **Kiểm tra Header bắt tay:** Đảm bảo `Request Headers` và `Response Headers` đều chứa `Upgrade: websocket`.
- **Kiểm tra qua CLI:** Sử dụng `wscat` để bỏ qua trình duyệt hoàn toàn: `wscat -c wss://example.com/ws`.

Mẹo về bảo mật và độ tin cậy

Tôi đã rút ra bài học xương máu rằng những lỗi nhỏ trong tệp tin khi triển khai có thể gây ra "lỗi bóng ma". Khi tôi đẩy các thay đổi Nginx lên production, tôi luôn kiểm tra tính toàn vẹn của tệp trước. Việc này giúp tiết kiệm hàng giờ debug những sai lệch cấu hình lẽ ra không nên tồn tại.

Tôi sử dụng công cụ Hash Generator trên ToolCraft để tạo mã checksum SHA-256 cho các tệp .conf của mình tại máy cục bộ. Vì nó xử lý mọi thứ trực tiếp trong trình duyệt, bạn không cần phải tải các logic nhạy cảm của server lên đám mây chỉ để lấy mã hash.

Lưu ý về Timeout

Nginx mặc định để proxy_read_timeout là 60 giây. Nếu người dùng của bạn phàn nàn rằng chat hoặc dashboard của họ bị ngắt kết nối mỗi phút một lần, thì đây chính là lý do. Tăng con số này lên 86400 (24 giờ) sẽ giữ cho đường truyền luôn mở, nhưng hãy nhớ triển khai cơ chế ping/pong (heartbeat) cơ bản trong ứng dụng của bạn để loại bỏ các kết nối thực sự đã chết.

Những điểm chính cần lưu ý

- WebSocket yêu cầu **HTTP/1.1**; chúng sẽ thất bại trên bản 1.0.
- Nginx là một "người gác cổng" nghiêm ngặt, mặc định sẽ loại bỏ các header hop-by-hop.
- Lỗi 400 là cách backend của bạn thông báo: "Tôi không nhận diện được đây là một yêu cầu WebSocket".

Related Error Notes