ByteScope

301

HTTP 301 Moved Permanently 怎麼修

3xx 轉址IETF 標準RFC 9110

舊網址退休了,接手的是 `Location` 裡的那一個——而 301 是少數幾種伺服器沒交代、快取也可以自己存起來的回應,所以送錯的 301 是最難收回來的一種轉址。

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

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

用 curl 重現 301

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

curl
curl -sS -D - -o /dev/null -L -w '\n%{num_redirects} hops, final %{http_code} at %{url_effective}\n' http://example.com/old-page

`-D -` 會把「每一跳」的回應標頭都印到標準輸出,不是只印最後一跳,`-o /dev/null` 把每個 body 丟掉,`-L` 跟著整條鏈走,`-w` 則印出總共跳了幾次、最後的狀態碼,以及回出那個狀態的網址。從上往下讀。每一條狀態行告訴你那一跳是 301 還是 302,而這決定了快取有沒有資格把它留著。每一個 `location:` 告訴你目標長什麼樣——一個 HTTPS 網站上出現絕對的 `http://`、而它又會轉回 HTTPS,那就是迴圈;出現內部主機名稱或多一個 `:8080`,那就是代理告訴應用程式錯的請求資訊。而在 301 那一跳上,找 `cache-control` 或 `expires`:兩個都沒有的話,這個回應照 RFC 9110 §15.1 就是可依啟發式規則快取的,而那一版轉址就是你的訪客會一直留著的那一版。還有兩招能讓結論變得明確。curl 沒有快取,所以瀏覽器會轉、這行指令不轉,那條轉址就只存在你這台機器上。另外 curl 自己的文件寫著,跟著 301、302 或 303 走的時候它會把 POST 降成 GET——所以同一行加上 `--post301` 再跑一次,比對伺服器紀錄,就能證明吃掉你請求 body 的是不是這條轉址。

301 實際上在講什麼

RFC 9110 §15.4.2 把 301 定義成:目標資源已經被指派了一個新的永久 URI,伺服器要產生一個 `Location` 欄位帶上那個首選的網址;而且它明白允許用戶端把自己手上指向舊 URI 的參照全部改寫掉。這個定義裡有三件事最常絆倒人。第一,「永久」是講給快取聽的承諾,不是你設定檔裡的一句註記:RFC 9110 §15.1 把 301 列在可依啟發式規則快取的狀態碼裡,所以一個完全沒帶 `Cache-Control` 的 301 照樣會被存起來、被重複使用——CDN 會、代理會、瀏覽器自己也會——而且之後不會再回頭問你一次。第二,方法不保證撐得過去:§15.4.2 寫著,基於歷史因素,用戶端「可以」在後續那個請求把方法從 POST 改成 GET,並且指名 308(RFC 7538)才是你不想要這種行為時該用的代碼。第三,`Location` 是一個 URI-reference,RFC 9110 §10.2.2 允許它是相對的、以目標 URI 為基準去解——所以一條在設定檔裡看起來沒問題的轉址,只要產生它的那個東西被餵了錯的請求資訊,還是可以把人送到錯的協定、錯的主機或錯的連接埠上。規格裡沒有任何機制讓你把一個已經被存在別人那裡的 301 收回來,而這一件事,就足以決定你今天到底要不要送出它。

301 最常被搞混的幾個代碼

容易搞混的怎麼分
308308(RFC 7538)是保證方法和 body 原封不動重送的永久轉址,而這正是 RFC 9110 §15.4.2 在 301 上不肯給的保證——它允許用戶端把 POST 變成 GET。兩者預設都可快取,所以 308 一樣黏、一樣收不回來;選哪一個是在選方法要不要保住,不是在選轉址能活多久。
302看得見的差別是「永久」,會花到錢的差別是快取。301 在 RFC 9110 §15.1 那份可依啟發式規則快取的清單裡,302 不在,所以沒帶明確 `Cache-Control` 的 302 不會被存起來,下一個請求你想改目的地就能改。這就是「先用 302 轉,確定了再升級」這個做法的全部理由。
HSTS不是每一次從 `http://` 跳到 `https://` 都是你的伺服器在轉。一台主機只要送過 `Strict-Transport-Security`,瀏覽器就會在任何請求離開這台機器之前先把網址改寫掉——Chrome 的網路面板會把它顯示成 `307 Internal Redirect`,附帶 `Non-Authoritative-Reason: HSTS`。它在伺服器那邊沒有一條對應的規則可以刪,所以你翻遍設定檔也找不到那條轉址。
Cloudflare轉址有可能在邊緣節點就發出去了,你的來源機根本沒被問到——Cloudflare 的 Always Use HTTPS、Bulk Redirects 和 Redirect Rules 都會產生這種轉址——所以一條在你伺服器設定裡完全找不到的 301,還是可能是真的。相對應的陷阱在另一個方向:Cloudflare 的文件寫著,Flexible 加密模式加上一台會把 HTTP 轉去 HTTPS 的來源機就是無限迴圈,因為來源機永遠看不到它要求的那個 HTTPS。
原因片語
Moved Permanently
類別
3xx 轉址
定義出處
RFC 9110
標準
IETF 標準

站上可以搭配的工具

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

相關狀態碼

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

常見問題

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

去讀回應標頭,不要讀那個網頁。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,會把值班的人送去堆疊裡錯的那一半。

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