HTTP 狀態碼
狀態碼是伺服器自己對「它拿這個請求做了什麼」所下的陳述。它是關於伺服器處理過程的證據,不是對請求內容的判決:它說明哪一層回答了你、目標有沒有被辨識出來,以及做出這個決定時身分是否已經建立。真正有用的部分幾乎都在相近碼之間的差別,因為那才是會改變你對伺服器認知的東西。
面板怎麼呈現狀態碼
| 位置 | 呈現形式 |
|---|---|
Network 記錄的 Status 欄 | 只有數字,依類別上色 |
Network 記錄的 Response 子分頁 | 起始行為 HTTP/1.1 <狀態碼>,沒有狀態文字 |
| Repeater 的回應區 | 起始行為 HTTP/1.1 <狀態碼>,狀態文字的位置永遠是空的 |
兩處都不會顯示狀態文字。Repeater 的起始行留了一個位置給它,但這個實驗室從來不填,所以你看到的是狀態碼後面跟著一個空白;Network 明細則連那個位置都沒有。在協定上,狀態文字由伺服器自行決定,屬於輔助說明,不是客戶端據以行動的狀態本身——但這裡的每一筆回應都沒有帶它。
Network 記錄的顏色分帶:
| 類別 | 顏色 |
|---|---|
| 500 以上 | 紅色 |
| 400–499 | 橘色 |
| 300–399 | 黃色 |
| 300 以下的一切,含 2xx 與 1xx | 綠色 |
顏色是拿數字比大小推出來的,所以沒人見過的狀態碼一樣會落進某一帶,而 1xx 與 2xx 共用綠色帶、沒有自己的顏色。這些控制項在面板上的位置寫在 Network 面板說明。
2xx —— 請求抵達了處理器,而處理器回答了
2xx 表示請求的格式足以被路由、伺服器要求的授權已經滿足,而且伺服器認為自己這一側的處理已經完成。
2xx 不代表的事:應用程式同意請求的內容。拒絕密碼的登入表單、找不到東西的搜尋、記下驗證錯誤的 API,都可以回 200 並把不同意寫在回應內容裡。狀態碼類別與應用程式層級的結果是兩條各自獨立的通道,而許多應用程式只用後面那一條。
| 狀態碼 | 伺服器陳述了什麼 | 它排除了什麼 |
|---|---|---|
200 OK | 有處理器執行過並產生了內容 | 該路徑未被路由;處理器前面擋著一道授權關卡 |
201 Created | 有處理器執行過,並宣告一份新資源已經存在 | 這是一次唯讀操作 |
202 Accepted | 請求被收下,但處理尚未結束 | 回應內容就是最終結果 |
204 No Content | 有處理器執行過,並刻意不產生內容 | 回應區空白就代表請求失敗 |
206 Partial Content | 伺服器接受了範圍請求 | 內容是整份資源 |
在只顯示回應內容的地方,204 與內容為空的 200 完全一樣。唯一能把「伺服器選擇不送內容」和「伺服器該送的內容沒送出來」分開的,就是那一行狀態行。
3xx —— 伺服器把客戶端導去別處,而不是直接回答
3xx 表示伺服器認得這個請求,並指名另一個位置,或斷定客戶端手上的快取仍然有效。路徑有被路由到。至於決定要轉址之前有沒有先檢查授權,狀態碼沒有說。
| 狀態碼 | 伺服器陳述了什麼 | 它排除了什麼 |
|---|---|---|
301 Moved Permanently | 資源有了新的正規位置,客戶端可以快取這次搬遷 | 原路徑仍是現行位置 |
302 Found | 這一次導向另一個位置,原路徑仍是正規位置 | 沒有排除任何關於搬遷是否長久的事 |
303 See Other | 不管這次用什麼方法,去那個位置時用 GET | 那個位置是同一次操作的延續 |
304 Not Modified | 條件式請求的驗證值相符,後面沒有內容 | 內容為空代表資源是空的 |
307 Temporary Redirect | 與 302 相同,但方法與內容必須保留 | 轉址會把 POST 悄悄降成 GET |
308 Permanent Redirect | 與 301 相同,但方法與內容必須保留 | 同一種降級,用於長久搬遷的情形 |
301 與 302 與 307 的差別
這三個碼把兩個互相獨立的軸擠在一起,搞混它們會讓整條轉址鏈看起來毫無規律。
| 搬遷是長久的 | 搬遷是暫時的 | |
|---|---|---|
方法可以被改寫成 GET | 301 | 302 |
| 方法與內容必須保留 | 308 | 307 |
301 與 302 的規格早於「方法是否保留」被釘死的時間,而客戶端最後一致採取把後續請求改寫成 GET 的做法。307 與 308 的存在就是為了把相反的意思明講出來。所以 POST 收到 302 與 POST 收到 307,產生的第二個請求並不相同——前者送出去時是沒有內容的 GET,後者是帶著原本內容的 POST。
另一個後果是快取:把 301 當真的客戶端,之後再訪時可能整個跳過原網址,於是一個你以為自己送出去的請求根本不會出現。
表單送出後收到 302 代表什麼
對憑證送出所回的轉址,成立的是「處理器執行過,並決定把客戶端移走」。它不成立「認證成功」——應用程式同樣可以在失敗時把人導回登入頁。能分辨這兩者的證據是 Location 標頭裡的目標,以及該筆轉址回應上的 Set-Cookie,不是狀態碼。
Browser 面板怎麼處理轉址
面板最多自動跟隨連續五個 3xx 回應。每一跳都是各自送出的請求,因此一條轉址鏈在 Network 記錄裡每一跳留下一列,而整條鏈在歷史紀錄裡只收斂成最終網址的一筆。
每一個被跟隨的跳轉一律以 GET 重新送出,與觸發它的狀態碼無關,也與原本的方法無關。面板的行為因此分辨不出 302 與 307;狀態碼本身,也就是 Network 記錄的 Status 欄,是唯一看得到這個差別的地方。
有兩種情形會讓鏈條中止而且不顯示任何錯誤訊息:
- 沒有
Location標頭的 3xx 不會被跟隨。它被當成一般回應直接呈現,而一個沒有內容的轉址呈現出來就是空白頁。 - 第六個連續的 3xx 也被這樣呈現——不顯示任何錯誤,而且網址列停在產生該次跳轉的那個網址,不會往前走。
4xx —— 伺服器拒絕了請求,並把責任歸給請求
4xx 表示伺服器把請求處理到足以做出「不給」這個決定的程度。這個決定本身就是資訊:它代表那個路徑上有東西在聽,也代表這次拒絕是刻意的,不是崩潰。
| 狀態碼 | 伺服器陳述了什麼 | 它排除了什麼 |
|---|---|---|
400 Bad Request | 請求無法被解析,或違反某項語法要求 | 請求以可用的形式抵達了應用程式邏輯 |
401 Unauthorized | 需要憑證,而憑證不存在或不被接受 | 資源是開放的;身分已經建立 |
403 Forbidden | 請求被理解了,然後被拒絕 | 已被接受的身分足以取得這份資源 |
404 Not Found | 伺服器對這個目標沒有任何可回應的內容 | 沒有排除「目標存在於某道授權層後面」 |
405 Method Not Allowed | 路徑有被路由,但不是給這個方法用的 | 路徑不存在:它對其他方法是存在的 |
409 Conflict | 請求與資源目前的狀態相衝突 | 請求格式有誤 |
413 Content Too Large | 內容超過了設定的上限 | 內容曾經被解析 |
415 Unsupported Media Type | Content-Type 不在這個處理器接受的範圍內 | 內容本身曾經被檢視 |
422 Unprocessable Content | 語法解析成功,但語意被拒絕 | 這是解析失敗:它是一次驗證決定 |
429 Too Many Requests | 撞到了流量限制 | 這一筆請求本身無效 |
401 與 403 的差別
差別在於做出決定的當下,身分是否已經建立。
401 | 403 | |
|---|---|---|
| 決定當下的身分 | 不存在,或不被接受 | 已建立,或被視為無關 |
| 伺服器實際在說 | 「我不知道你是誰」 | 「我知道的夠多了,答案還是不行」 |
WWW-Authenticate 標頭 | 規格要求要有 | 不預期出現 |
401 成立的是「這條路徑在一道認證關卡後面」,而這件事同時代表路徑存在、伺服器知道怎麼路由它。403 成立的是「缺的不是身分」——同一個請求換上更好的憑證,仍然會撞到同一條規則。
應用程式很常把這兩者用混,所以一個該回 401 卻回 403 的應用程式,這件事本身是對它自身一致性的觀察,而不是關於它存取模型的事實。
404 與 403 的差別
兩者都在說「這個你拿不到」,差別在於伺服器願意透露多少。
403確認了:就路由層而言目標是存在的,而且有一條規則拒絕了它。404對存在與否什麼都沒說。伺服器會對從未被路由的路徑回404,也會對存在但被設定藏起來、不給未授權客戶端看的路徑回404,而後者是披著404外皮的403語意。
後果是不對稱的:403 是存在的正面證據,而 404 除了「沒有可供回應的內容」之外什麼都證明不了。把回應的大小、標頭與耗時拿去跟一條已知未被路由的路徑相比,才是分開這兩種情形的東西;光看狀態碼分不出來。
405 作為存在訊號
405 是這四個裡面歧義最少的。它只可能由「路徑比對成功、然後找不到對應這個方法的處理器」的路由器產生,所以它同時成立兩件事:路徑有被路由,而且那裡至少有另一個方法是被處理的。Allow 標頭若有出現,會直接把那些方法列出來。
5xx —— 伺服器壞了,而且它說出來了
5xx 表示請求已被認定為「客戶端該負的責任都盡到了」,然後伺服器這一側有東西失敗。類別邊界很重要:4xx 是一次決定,5xx 是一次故障。這個區別正是 5xx 之所以有價值的原因——它通常代表某個輸入抵達了一段當初沒被寫來處理它的程式碼。
| 狀態碼 | 伺服器陳述了什麼 | 它排除了什麼 |
|---|---|---|
500 Internal Server Error | 應用程式本身拋出了未被處理的狀況 | 請求是被某條驗證規則擋掉的 |
501 Not Implemented | 伺服器根本不支援這個方法 | 失敗只發生在這條路徑上 |
502 Bad Gateway | 代理層從上游收到無效的回答,或根本沒有回答 | 失敗發生在回答你的那一層 |
503 Service Unavailable | 回答你的那一層活著,但宣告自己無法服務 | 請求曾被處理 |
504 Gateway Timeout | 代理層等了上游,然後放棄 | 上游拒絕了:它根本沒有回答 |
500 與 502 與 504 的差別
三者都代表「沒有得到有用的回答」,但它們指名的是三個不同的失敗者。
| 誰失敗了 | 抵達了什麼 | |
|---|---|---|
500 | 處理這個請求的應用程式 | 應用程式執行了,並拋出了東西 |
502 | 上游某一跳,回答得很糟 | 代理層拿到一個它用不了的回覆 |
504 | 上游某一跳,沒有回答 | 代理層在時限內沒拿到任何回覆 |
三者之中只有 500 成立「應用程式執行過你的輸入」。502 與 504 成立的是:某個東西前面站著一層代理,而後面那個東西的狀態與你正在對話的那一層並不相同。兩者之間,502 代表有回答送到而被判定不可用,504 代表等待時間用完了——後者由逾時界定,所以 Network 記錄 Time 欄的耗時本身就是證據的一部分。
500 的回應內容帶著什麼
伺服器端未被處理的例外,經常會把例外本身序列化進回應內容:堆疊追蹤、檔案路徑、樣板名稱,或資料庫驅動的錯誤文字。那段內容陳述的是應用程式壞掉當下正在做什麼,而資料庫錯誤訊息更進一步陳述了輸入抵達了一次查詢——辨識它的判準寫在 SQL injection。
回應內容為空或只有制式文字的 500,成立的是故障本身而沒有指名故障內容。細節的缺席是錯誤處理器的性質,不是故障的性質。
這個實驗室自己產生的狀態碼
Network 記錄裡有些回應根本不是題目應用程式送的。它們產生在實驗室自己的 dispatch 邊界上,把它們讀成應用程式的回答,會讓你去找一個並不存在的行為。
| 狀態碼 | 回應內容 | 代表什麼 |
|---|---|---|
503 | {"error": "runtime not ready"} | 頁面的 dispatch 層當下還沒有 runtime。這個請求從未抵達題目應用程式 |
503 | {"error": "<host 自己的錯誤訊息>"} | runtime host 沒有 worker 可以接手這個請求。與另外三列不同,error 的值不是固定字串,而是 host 當下拋出的訊息。這份回應是被刻意合成出來的,好讓這次嘗試仍以一列的形式留在記錄裡,而不是整筆消失 |
508 | {"error": "challenge runtime execution exceeded the time limit"} | 題目應用程式收到了請求,但在單一請求十秒的上限內沒有回答 |
502 | {"error": "the challenge runtime could not be reached"} | 在 Code 面板由 requests 收到:runtime 連不上。它以回應物件的形式抵達,不是拋出例外 |
上面這些之外還有另一個訊號:只有 PHP 執行環境會在展開後的 Network 明細上方掛一塊紅框,開頭寫著 Runtime diagnostic:。它寫的是這個執行環境在處理這份回應時丟掉了什麼——例如附在不能帶主體的狀態碼上的輸出、或是被移除的保留標頭——所以它不代表直譯器出錯。本 repo 現存題目的後端全是 Python,因此目前沒有任何一題看得到它。
由此有兩個後果。第一,記錄裡的一列紅字不會自動成為關於題目應用程式的證據。第二,在 Code 面板看到的 502 與題目應用程式送出的 502 意思不同,而分開兩者的是回應內容的文字。