405
路徑在,方法伺服器也認得——它只是不接受你在這裡用這個方法,而且它有義務把它願意收的方法清單交給你。
同樣三個數字,你站在請求的哪一邊,它就是不同的問題。直接讀你自己那一塊。
為什麼會看到它
瀏覽器裡最常見的 405,是一個你從來沒寫過的請求。跨來源、帶 JSON content type 或自訂標頭的請求會走預檢,所以瀏覽器會先送一個 `OPTIONS`,而一台路由器裡只註冊了 GET 和 POST 的伺服器,會用 405 回答那次預檢。接著你在 console 看到的是 CORS 錯誤,因為預檢沒成功——而那則 CORS 訊息把底下的 405 蓋住了,於是大家為了一個「路由沒註冊」的問題,花好幾個小時去調 `Access-Control-Allow-Origin`。第二種樣子是 POST 變成 GET 抵達。RFC 9110 §15.4.2 記載,用戶端在歷史上跟著 301 或 302 走時會把 POST 改寫成 GET,所以一個送往 `http://` 網址、再被轉到 `https://` 的表單,或是一個被轉去補上結尾斜線的路徑,到伺服器手上時方法整個就不對了。
該怎麼做
什麼都別做之前,先在網路面板裡讀失敗那個請求的方法,因為那常常不是你呼叫的那個方法。如果它寫的是 OPTIONS,那工作在伺服器端——用 CORS 標頭加一個 204 去回答預檢——你改 `fetch` 是不會有幫助的。接著讀那個 405 上的 `allow` 標頭:§15.5.6 規定它是必要的,而它就是「這條路徑收哪些方法」的直接答案,通常當場就能結案。如果鏈上有轉址,就直接把請求送到最終網址;而且任何可能是 POST 的東西,要優先選會用 307 或 308 的伺服器,因為那是唯二保住方法的轉址。最後修好之後記得清邊緣快取,因為在這裡「405 被快取起來」是真的會發生的事。
為什麼會看到它
路由器對上了路徑、沒對上方法——或者其中之一在進來的路上被改寫了。各家框架講這件事的方式不一致:有些回 405 並附上正確的 `Allow`,有些對沒對上的方法直接回 404,所以同一個 bug 在你自己兩個服務裡會長成兩種不同的問題。靜態檔案服務自成一格:nginx 的靜態模組只服務 GET 和 HEAD,所以一個打到「解析後落在磁碟檔案上」的路徑的 POST,會由 nginx 回 `405 Not Allowed`,不是你的應用程式回的——這就是為什麼那個請求在你的應用程式紀錄裡從來沒出現過。路徑改寫負責剩下的部分:一條加上或去掉結尾斜線的 rewrite,可以把一個 POST 挪到一條只註冊了 GET 的路由上;而一台把 `/api/users` 轉去 `/api/users/` 的代理,等於在路上給了用戶端一個把方法降級的機會。
該怎麼做
把路由表印出來,把方法和路徑「成對」比對,不要當成兩件獨立的事——每個框架都有這個指令,而且比讀路由器原始碼快。確認你的 405 真的有帶 `Allow`,因為那是 MUST,而且是這個回應能講的最有用的一句話;一個省略它的框架,等於把一個一分鐘就能在用戶端修好的問題,變成一次客服對話。特別要檢查你有沒有不小心對 `HEAD` 或 `OPTIONS` 回 405:§9.1 要求通用伺服器支援 GET 和 HEAD,而 OPTIONS 是每一次瀏覽器預檢在用的,所以這兩個最有可能弄壞你沒測到的東西。如果那個 405 從來沒進到你的應用程式紀錄,那就改去看靜態檔案處理器和 rewrite 規則。修完之後清 CDN,因為 §15.1 讓這個回應可以被啟發式快取。
為什麼會看到它
這一個很不可能是你造成的。它通常出現在你送出表單之後——本來該看到確認畫面,結果拿到一頁短短的錯誤頁——意思是網站的表單指向了一個不收送出資料的網址。它也可能出現在你點了一個舊書籤,或是一條在 `www.` 和沒有 `www.` 兩個版本之間切換的連結之後,因為路上那次轉址會把你送出的資料變成一次普通的頁面請求。你的瀏覽器、你的裝置、你填的資料,都沒有問題。
該怎麼做
不要重新整理那一頁錯誤頁——重新整理會把同一份被拒絕的資料再送一次,結果一模一樣,而且有些瀏覽器還會跳出「要重新送出嗎」的警告。回到表單,從網站首頁用 `https://` 開始走一遍,讓路上不會有任何轉址,然後送出一次。如果網站同時有 `www.` 和非 `www.` 的網址,就用網站自己連結用的那一個。如果還是失敗,那就是網站那邊接線接錯了,而你回報時真正有用的資訊是:你當時在哪一頁、按了哪個按鈕。
對著出錯的那個網址跑一次。它只印狀態碼、不印錯誤頁,所以你看到的是伺服器真正說了什麼,而不是瀏覽器畫出來的東西。
curl -sS -X POST -o /dev/null -D - -w '\n%{http_code}\n' https://example.com/`-X POST` 會取代 curl 本來會用的方法,`-o /dev/null` 把錯誤頁丟掉,`-D -` 把回應標頭印出來——答案就在那裡。要找的標頭是 `allow`:§15.5.6 規定它在 405 上是必要的,而且它列的是那條路徑真正接受的方法,所以跑一次就知道是你的呼叫錯了還是你的路由錯了。它「不在」本身也是一項發現——一個沒有 `Allow` 的 405 代表伺服器沒照規格回答;而一個存在但為空的 `Allow`,合法地代表這個資源什麼都不收。有兩個變體值得多花幾秒。`curl -I` 送的是 HEAD 不是 GET,所以你用 `-I` 拿到的 405 有可能只跟 HEAD 有關,跟你真正在意的那個方法無關——而在 GET 可行的情況下,§9.1 會把那個當成伺服器缺陷。另外,與其猜瀏覽器的預檢,不如直接重現它:跑 `curl -sS -D - -o /dev/null -X OPTIONS -H 'origin: https://app.example.com' -H 'access-control-request-method: POST' https://example.com/api/`——那裡出現的 405,就是那則從來沒提過它的 CORS 訊息背後真正的原因。
RFC 9110 §15.5.6 把 405 定義成:請求行裡的方法來源伺服器認得,但目標資源不支援;而且它把有用的那一半訂成強制的——來源伺服器在 405 回應裡「必須」產生一個 `Allow` 標頭欄位,列出目標資源目前支援的方法。`Allow` 定義在 §10.2.1,裡面有一個細節值得在你誤讀之前先知道:空的 `Allow` 欄位值是合法的,意思是這個資源一個方法都不支援,那跟「標頭不在」是兩回事。旁邊兩個代碼替它劃出了邊界。§15.6.2 把 501(Not Implemented)留給「伺服器對任何資源都不支援的方法」,所以 405 是比較窄的講法:這個方法伺服器認得,只是不在這裡。而 §9.1 要求所有通用伺服器都必須支援 GET 和 HEAD,所以一個 GET 可以、HEAD 卻回 405 的資源是缺陷、不是政策——這一點值得記住,因為 `curl -I` 送的是 HEAD,也就是說你第一個抓來用的工具,有可能自己製造出你正在查的那個代碼。最後一個性質是會活得比那次部署還久的:405 是 RFC 9110 §15.1 列為可依啟發式規則快取的狀態碼之一,跟 404、410、501 站在一起,這代表快取或 CDN 在完全沒有明確 `Cache-Control` 的情況下也可以把它存起來。所以一條在某次爛部署期間回了十分鐘 405 的路由,可以在部署修好之後,繼續從邊緣節點回 405,直到那份存起來的回應過期、或有人去清掉為止——跟 404 是同一個陷阱,理由也一樣。
| 容易搞混的 | 怎麼分 |
|---|---|
501 | 501(Not Implemented,§15.6.2)的意思是伺服器對任何資源都不支援這個方法;405 的意思是它支援這個方法,只是不支援在這個資源上。實務上的差別在你下一步往哪走:從代理或一台老伺服器拿到 `PATCH` 的 501,是一個你要繞過去的能力缺口;而從你自己的應用程式拿到 `PATCH` 的 405,是一條你沒註冊的路由。兩者在 §15.1 底下都可以被啟發式快取,所以兩者都可能活得比造成它的原因還久。 |
| 404 | 一個對沒對上的方法回 404 的路由器,把答案裡比較有用的那一半藏起來了,而 §15.5.6 正是這個區別重要的理由:405 會帶 `Allow`,404 什麼都不帶。測試只要一行指令——某條路徑對 POST 回 404、對 GET 回 200,那它就是一個披著錯誤號碼的 405,你該看的是方法,不是路徑。 |
| 301 | 你眼前這個 405 有可能是轉址的副產物,不是你這次呼叫的問題。§15.4.2 記載用戶端在歷史上跟著 301 走時會把 POST 改寫成 GET,實務上 302 也一樣,所以一個橫跨 `http://` 到 `https://`、或橫跨結尾斜線轉址的 POST,可能會以 GET 的身分抵達一條只收 POST 的路由。307 和 308 才是會保住方法的轉址,這就是 API 該用它們的理由。 |
CORS preflight | 一個帶 JSON body 或自訂標頭的跨來源請求,前面會有一個瀏覽器自己產生的 `OPTIONS` 請求,而一台沒有 OPTIONS 路由的伺服器會用 405 回答它。接著瀏覽器報的是 CORS 失敗,因為預檢沒成功——所以那則訊息講的是錯的問題,而網路面板上失敗那一列上面一列的那個 405,才是真的。 |
Allow | §10.2.1 把 `Allow` 定義成目標資源支援的方法清單,§15.5.6 則規定 405 上一定要送它——所以它不在,是伺服器的 bug,不是什麼提示。空值要小心讀:空的 `Allow` 欄位值是一種合法的講法,意思是這個資源一個方法都不支援,那跟標頭根本不存在是兩種不同的陳述。 |
這些全都在你的瀏覽器裡跑,不會上傳任何東西。
查 405 的時候,通常也會順手看一下這幾個。
去讀回應標頭,不要讀那個網頁。curl -sS -o /dev/null -D - https://example.com/path 會只印標頭、不印內容,而 server、via 和 cf-ray 三個加起來,就能指出是哪一層回答的。有 cf-ray 的值代表這個回應是 Cloudflare 處理的,那個值也是他們客服會跟你要的編號。想把邊緣節點整個排除掉,就加上 --resolve example.com:443:203.0.113.10 再打一次:它會照樣送出原本的主機名稱和 SNI,但直接連到你指定的來源位址——答案變了,就代表邊緣節點和來源機講的不是同一件事。
不重要,而且絕對不要拿它來做判斷。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,會把值班的人送去堆疊裡錯的那一半。
405 還是卡住嗎?看完整的狀態碼對照表,或是回到上面,讀為你這一端寫的那一塊。