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

Ảnh chụp đoạn mã nền tối giải thích mã trạng thái và redirect 301 vs 302 vs 307 308, năm lớp mã trạng thái theo chữ số đầu 1xx thông tin 2xx thành công 3xx chuyển hướng 4xx lỗi phía client 400 sai 401 403 quyền 404 không thấy 429 quá nhiều 5xx lỗi phía server 500 lỗi 502 gateway 503 quá tải, bốn mã redirect khác nhau ở vĩnh viễn và giữ method 301 Moved Permanently vĩnh viễn có POST không giữ đổi thành GET, 302 Found tạm không vĩnh viễn POST không giữ đổi GET, 307 Temporary Redirect tạm POST giữ nguyên, 308 Permanent Redirect vĩnh viễn POST giữ nguyên, 301 308 vĩnh viễn trình duyệt SEO nhớ lâu cân nhắc kỹ, curl và redirect curl mặc định không theo redirect in 3xx curl -L đi theo Location tới đích curl -L -d POST curl tự đổi method theo mã lưu ý -X POST ép method mọi chặng che mất hành vi đổi GET của 302

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ã:

Ảnh chụp bảng kết quả đo thật nền tối chạy curl 7.88, dich in ra method mà nó nhận được sau redirect, phần một mặc định curl không theo redirect curl -i http 127.0.0.1 8097 r302 trả HTTP/1.1 302 Found Location slash dich Content-Length 0 in 302 dừng không tự đi, phần hai POST bằng -d rồi -L method có đổi không curl -L -d x=1 r301 đến đích bằng GET, curl -L -d x=1 r302 đến đích bằng GET đổi POST thành GET, curl -L -d x=1 r307 đến đích bằng POST, curl -L -d x=1 r308 đến đích bằng POST giữ POST, phần ba vòng lặp redirect loopA sang loopB sang loopA curl -L http 127.0.0.1 8097 loopA trả curl 47 Maximum 50 redirects followed curl chặn ở 50 chặng max-redirs để đổi giới hạn

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 curl không đi theo redirect — nó in 302 Found kèm Location rồi dừng. Phải có -L mới theo.
  • 301 và 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.
  • 307 và 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ề

  1. 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.
  2. 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.
  3. 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ặc X-Forwarded-Proto sai; và 301/308 rất "dính" nên chỉ dùng khi chắc chắn đổi lâu dài.

Nguồn

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.