ByteScope

302

HTTP 302 Found 怎麼修

3xx 轉址IETF 標準RFC 9110

一次暫時的繞路:資源還是屬於你要的那個網址,只是現在伺服器想從別的地方把它送出來——而你的瀏覽器會直接繞過去,完全不會讓你的程式碼看到這一跳。

為什麼會看到 302,以及該怎麼處理

同樣三個數字,你站在請求的哪一邊,它就是不同的問題。直接讀你自己那一塊。

用 curl 重現 302

對著出錯的那個網址跑一次。它只印狀態碼、不印錯誤頁,所以你看到的是伺服器真正說了什麼,而不是瀏覽器畫出來的東西。

curl
curl -sS -D - -o /dev/null -L -d 'user=me' -w '\nfinal %{http_code} after %{num_redirects} hops at %{url_effective}\n' https://example.com/login

`-d` 會送出一個真的請求 body,這樣本身就是 POST——這裡刻意不加 `-X`,因為 curl 文件寫明它會套在整條鏈的每一個請求上,正好會蓋掉這行指令想讓你看到的那次方法改寫。`-L` 跟著整條鏈走,`-D -` 把每一跳的標頭都印出來,`-w` 則印出最後的狀態碼、跳了幾次,以及回答的那個網址。這份輸出裡有四樣東西決定你下一步。第一條狀態行把 302 跟 303、307 分開,而那是「方法可能會變」、「後續請求就是 GET」、「原封不動重送」三種契約的差別。`location:` 的值告訴你應用程式是不是拿對的請求資訊去組網址——HTTPS 網站上出現 `http://`、內部主機名稱,或一個只有容器裡才存在的連接埠,三者都指向少了 `X-Forwarded-Proto` 或 `X-Forwarded-Host`。同一個回應上的 `set-cookie:` 則是登入迴圈的判決點:在純 HTTP 上送 `Secure`、`SameSite=None` 沒配 `Secure`,或者 `Domain` 跟目的地不同源,都會讓下一跳帶著空的 session 過去,然後再彈一次。至於最後那個回應的 `content-type`,就是你前端看不到的那個關鍵:你要 JSON 卻拿到 `text/html`,那就是登入彈跳。curl 的文件寫著,它跟著 301、302 或 303 走的時候會把 POST 降成 GET,其他 3xx 則保留方法——所以同一行加上 `--post302` 再跑一次,比對伺服器紀錄,就能證明弄丟你欄位的是不是那次方法改寫。

302 實際上在講什麼

RFC 9110 §15.4.3 把 302 定義成:目標資源暫時待在另一個 URI 底下;接著補了一句讓它和 301 分家的指示——因為這個轉址偶爾會變,用戶端之後仍然應該繼續用原本的目標 URI。同一節也帶著那個歷史包袱:用戶端「可以」把方法從 POST 改成 GET,而 307(Temporary Redirect)被指名為你不想要那樣時該用的代碼。規格沒說的部分一樣有用:302 不在 RFC 9110 §15.1 那份可依啟發式規則快取的狀態碼清單裡,所以除非你自己掛上 `Cache-Control`,302 不會被存起來,每個請求都會重問一次。這個性質,正是它適合拿來反覆調整的原因。在你動手寫登入流程之前,還有一個區別要先分清楚:§15.4.4 把 303(See Other)定義成「伺服器正把用戶端導向另一個資源,由 `Location` 欄位裡的 URI 指出,用意是對原始請求提供一個間接的回應」,而且明訂後續那個請求要用 GET。多數框架在 post-redirect-get 的時候吐的是 302,但真正表達那個意思的代碼是 303。

302 最常被搞混的幾個代碼

容易搞混的怎麼分
307307(Temporary Redirect)就是保證方法不變的 302。RFC 9110 §15.4.3 基於歷史因素,允許用戶端在 302 上把 POST 變成 GET;§15.4.8 定義 307,用意就是讓方法和 body 原封不動重送。一樣是暫時的,契約不一樣——而如果你的轉址剛好夾在表單送出的中間,那份契約就是你的請求 body。
303303(See Other,§15.4.4)的意思是「你這個請求的答案在另一個 URI 上」,而且後續那一次是 GET。這正是 post-redirect-get 真正在講的事,而且它是明確的,302 只是「允許」同樣的行為。POST 成功之後要轉址,就用 303 把意圖寫清楚,不要靠一條歷史包袱來的允許條款。
301使用者看得到的差別是永不永久,實務上的差別是快不快取。302 不在 RFC 9110 §15.1 那份可依啟發式規則快取的狀態碼清單裡,所以沒帶明確 `Cache-Control` 就永遠不會被存起來,你下次部署想改目的地就能改。301 會被存起來,而且沒有任何辦法把它從訪客的瀏覽器裡清掉。先用 302 反覆調,確定了再升級。
401一個用「轉去登入頁的 302」回答過期 session 的 API,會把每一次認證失敗都變成一個裝滿 HTML 的 200,因為瀏覽器在你的程式碼看到任何東西之前就先跟著轉址走了。改成 401 加一個 JSON body——以及照 RFC 9110 §15.5.2 附上 `WWW-Authenticate` 標頭——才能讓必須處理它的那個用戶端讀得懂這次失敗。
原因片語
Found
類別
3xx 轉址
定義出處
RFC 9110
標準
IETF 標準

站上可以搭配的工具

這些全都在你的瀏覽器裡跑,不會上傳任何東西。

相關狀態碼

查 302 的時候,通常也會順手看一下這幾個。

常見問題

怎麼知道這個狀態碼是哪一台伺服器回的?

去讀回應標頭,不要讀那個網頁。curl -sS -o /dev/null -D - https://example.com/path 會只印標頭、不印內容,而 serverviacf-ray 三個加起來,就能指出是哪一層回答的。有 cf-ray 的值代表這個回應是 Cloudflare 處理的,那個值也是他們客服會跟你要的編號。想把邊緣節點整個排除掉,就加上 --resolve example.com:443:203.0.113.10 再打一次:它會照樣送出原本的主機名稱和 SNI,但直接連到你指定的來源位址——答案變了,就代表邊緣節點和來源機講的不是同一件事。

數字後面那句原因片語(像 Not Found)重要嗎?

不重要,而且絕對不要拿它來做判斷。RFC 9112 要求用戶端忽略原因片語,伺服器可以隨意改它,而 HTTP/2 和 HTTP/3 根本沒有這個欄位——所以在 HTTP/1.1 上是 404 Not Found,到了 HTTP/2 就只剩一個 404。這個網站會寫出登記在案的片語,是因為那是大家會拿去搜尋、也是會出現在 HTTP/1.1 紀錄裡的字串,不是因為有任何軟體依賴它。

這個代碼失敗的請求該不該重試?

5xx 和 429 可以重試,4xx 不要——下一次送過去也不會有任何不同。如果回應帶了 Retry-After 就照它做,RFC 9110 定義它就是為了這件事,值可以是秒數也可以是一個時間點。沒有的話,就用帶抖動的指數退避加上一個硬上限,而且只對冪等的方法這樣做:一個被重試的 POST 可能會刷兩次卡。對一台已經在失敗的伺服器丟出重試風暴,是短暫事故變成長時間事故最常見的那條路。

我可以讓自己的伺服器改回別的狀態碼嗎?

可以,唯一要守的規則是那個代碼必須是真的。所有會自動讀你回應的東西——搜尋引擎、監控、快取、用戶端的重試邏輯——都只看那個數字,從來不看那一頁寫了什麼。用 200 回一頁錯誤訊息,會把故障從你自己的警報裡藏起來;用 200 回一頁不存在的內容,會讓錯誤被當成內容收進索引;而對一個只是格式不對的請求回 500,會把值班的人送去堆疊裡錯的那一半。

302 還是卡住嗎?看完整的狀態碼對照表,或是回到上面,讀為你這一端寫的那一塊。