Mọi lập trình viên biết 404 (không thấy) và 500 (server lỗi). Nhưng vào phần redirect — 301, 302, 307, 308 — thì phần lớn mơ hồ, và chọn sai gây hậu quả thật: một biểu mẫu POST bỗng thành GET và mất dữ liệu, hay một redirect vĩnh viễn khiến trình duyệt và Google nhớ sai địa chỉ hàng tháng trời. Điểm mấu chốt không chỉ là "vĩnh viễn hay tạm thời" như nhiều người nghĩ, mà còn là có giữ nguyên phương thức HTTP hay không. Bài này đo thật khác biệt đó với curl 7.88.
Năm lớp mã trạng thái
Chữ số đầu tiên phân loại:
- 1xx thông tin (hiếm gặp trực tiếp, ví dụ 100 Continue, 101 Switching Protocols).
- 2xx thành công (200 OK, 201 Created, 204 No Content).
- 3xx chuyển hướng (301/302/307/308 — chủ đề bài này, và 304 Not Modified cho cache — bài sau).
- 4xx lỗi phía client (400 request sai, 401/403 xác thực/quyền, 404 không thấy, 429 quá nhiều request).
- 5xx lỗi phía server (500 lỗi nội bộ, 502 bad gateway, 503 quá tải).
Ranh giới 4xx/5xx quan trọng khi debug: 4xx nghĩa là "bạn gửi sai", 5xx nghĩa là "server hỏng" — nhìn nhầm là sửa nhầm chỗ.
Bốn mã redirect: hai trục, không phải một
Nhiều người chỉ nhớ "301 vĩnh viễn, 302 tạm thời". Nhưng có hai trục độc lập:
Mã Ý nghĩa Vĩnh viễn? Giữ nguyên POST?
301 Moved Permanently có KHÔNG (đổi -> GET)
302 Found (tạm) không KHÔNG (đổi -> GET)
307 Temporary Redirect không CÓ (giữ POST)
308 Permanent Redirect có CÓ (giữ POST)
301/308 là vĩnh viễn — trình duyệt cache lại, công cụ tìm kiếm chuyển "điểm SEO" sang URL mới, nên chỉ dùng khi thật sự đổi địa chỉ lâu dài. 302/307 là tạm thời. Trục thứ hai — giữ POST hay không — mới là cái hay bị bỏ sót và gây lỗi.
curl http://... # mặc định KHÔNG theo redirect, chỉ in mã 3xx
curl -L http://... # -L: đi theo header Location tới đích
curl -L -d "x=1" ... # -d: gửi POST, curl tự quyết method khi redirect

Hình 1: Năm lớp mã trạng thái, và bốn mã redirect trên hai trục — "vĩnh viễn?" và "có giữ POST?". curl -L đi theo redirect; -d để POST đúng cách.
Đo thật: POST có bị đổi thành GET không?
Dựng server trả các redirect tới /dich, và /dich in ra phương thức nó nhận được. Gửi POST (bằng -d, để curl áp đúng luật) qua từng mã:

Hình 2: Mặc định curl không theo redirect (in 302 rồi dừng). Với -L -d: 301 và 302 đến đích bằng GET (đổi method), 307 và 308 đến đích bằng POST (giữ nguyên). Vòng lặp redirect dừng ở curl: (47) Maximum (50) redirects followed.
Kết quả rõ ràng và đúng chuẩn:
- Mặc định
curlkhông đi theo redirect — nó in302 FoundkèmLocationrồi dừng. Phải có-Lmới theo. 301và302+ POST → đến đích bằng GET. Đây là hành vi lịch sử (từ thời trình duyệt cũ): sau redirect, body POST bị bỏ, phương thức thành GET. Nếu bạn POST một biểu mẫu tới endpoint trả 302, dữ liệu POST không được gửi tiếp — một lỗi âm thầm.307và308+ POST → đến đích bằng POST. Hai mã này ra đời chính để sửa sự mơ hồ đó: chúng cam kết giữ nguyên phương thức và body. Khi cần redirect một POST mà không mất dữ liệu, dùng 307/308.
Cạm bẫy -X POST che mất hành vi thật
Một chi tiết đáng nhớ khi test bằng curl: -X POST ép phương thức POST lên mọi chặng, kể cả sau redirect — nên nó che mất hành vi đổi-thành-GET của 301/302 (bạn sẽ thấy POST ở đích dù đáng lẽ là GET). Muốn quan sát đúng luật redirect, dùng -d (hoặc -F) để curl tự áp logic phương thức. Đây là lý do nhiều người test thấy "302 vẫn giữ POST" và kết luận sai.
Vòng lặp redirect
Nếu /loopA trỏ tới /loopB và /loopB trỏ ngược lại /loopA, client sẽ đi vòng vô tận. curl chặn điều này: mặc định tối đa 50 chặng, vượt thì báo curl: (47) Maximum (50) redirects followed. Trình duyệt cũng có giới hạn tương tự ("ERR_TOO_MANY_REDIRECTS"). Vòng lặp redirect thường do cấu hình sai: ví dụ rule "thêm dấu / cuối" và rule "bỏ dấu / cuối" cùng bật, hay HTTP→HTTPS→HTTP do proxy không truyền đúng X-Forwarded-Proto (bài nginx sẽ gặp lại). Đổi giới hạn bằng --max-redirs.
Đánh đổi và lưu ý
301/308 rất "dính". Vì vĩnh viễn, trình duyệt cache mạnh và có thể nhớ tới khi hết hạn/xóa cache thủ công; công cụ tìm kiếm chuyển hẳn index. Đặt nhầm 301 rồi muốn đổi lại là ác mộng. Khi chưa chắc, dùng 302/307 (tạm thời) — đảo ngược dễ hơn nhiều.
Chọn mã theo phương thức bạn cần bảo toàn. GET redirect thì 301/302 hay 307/308 đều ổn (GET không có body). Nhưng redirect một POST/PUT/DELETE mà muốn giữ nguyên hành động thì bắt buộc 307/308.
Redirect tốn một vòng khứ hồi (round-trip). Mỗi redirect là một request–response thêm. Chuỗi redirect dài làm chậm rõ rệt, nhất là trên mạng di động độ trễ cao. Gộp redirect (trỏ thẳng tới đích cuối) khi có thể.
Ba ý mang về
- Redirect có hai trục, không phải một: vĩnh viễn (301/308) hay tạm (302/307), và có giữ POST hay không — 301/302 đổi POST thành GET, 307/308 giữ nguyên (đo thật). Redirect một biểu mẫu POST qua 302 là mất dữ liệu âm thầm.
- curl mặc định không theo redirect: cần
-L; và-X POSTép method mọi chặng nên che mất hành vi đổi-GET của 302 — dùng-dđể thấy luật thật. - Vòng lặp redirect dừng ở giới hạn: curl
curl: (47) Maximum (50) redirects followed, trình duyệt báo lỗi tương tự — thường do rule cấu hình mâu thuẫn hoặcX-Forwarded-Protosai; và 301/308 rất "dính" nên chỉ dùng khi chắc chắn đổi lâu dài.
Nguồn
- MDN — Redirections in HTTP và HTTP response status codes: https://developer.mozilla.org/en-US/docs/Web/HTTP/Redirections
- RFC 9110 mục Redirection 3xx: https://www.rfc-editor.org/rfc/rfc9110#name-redirection-3xx
Phần sau ta đo cái giá của việc mở kết nối: TCP handshake tốn bao nhiêu, và vì sao HTTP keep-alive (tái dùng kết nối) tạo khác biệt lớn — xem thật bằng curl.