一直沒有很深入了解 3xx Status Code 有哪些,各代表什麼含意,就趁這篇文章把它搞懂吧!
The server's response payload usually contains a short hypertext note with a hyperlink to the new URI(s).
我們使用 NodeJS HTTP Server 實作(但我們不按照 RFC7231 的建議)
import httpServer from "../httpServer";
import { faviconListener } from "../listeners/faviconListener";
import { notFoundListener } from "../listeners/notFoundlistener";
httpServer.on("request", function requestListener(req, res) {
if (req.url === "/favicon.ico") return faviconListener(req, res);
// 轉導後的目標網址,回傳 { method, body }
if (req.url === "/") {
const chunks: Buffer[] = [];
req.on("data", (chunk) => {
chunks.push(chunk);
});
req.on("end", () => {
res.setHeader("Content-Type", "application/json");
res.end(
JSON.stringify({
method: req.method,
body: Buffer.concat(chunks).toString(),
}),
);
return;
});
return;
}
if (req.url === "/301") {
res.statusCode = 301;
res.setHeader("location", "http://localhost:5000");
res.end();
return;
}
return notFoundListener(req, res);
});
先用瀏覽器打開 http://localhost:5000/301 做個簡單的 GET 請求
再來用 fetch 做個 Post + Body 的請求
fetch("http://localhost:5000/301", {
method: "POST",
body: "hello-world",
redirect: "follow",
headers: { "Content-Type": "text/plain" },
})
.then((res) => res.text())
.then((body) => console.log(body));
轉導的 HTTP Request 確實變成 GET,且 Body 被捨棄
<form method="POST" action="http://localhost:5000/308">
技術的網站,若網站網址更動,就會需要用到 308,而不是 301,也算是為了向後兼容(backward compatibility)新增以下 NodeJS 程式碼
if (req.url === "/308") {
res.statusCode = 308;
res.setHeader("location", "http://localhost:5000");
res.end();
return;
}
再來用 fetch 做個 Post + Body 的請求
fetch("http://localhost:5000/308", {
method: "POST",
body: "hello-world",
redirect: "follow",
headers: { "Content-Type": "text/plain" },
})
.then((res) => res.text())
.then((body) => console.log(body));
轉導的 HTTP Request Method 跟 Body 確實不變
Note: For historical reasons, a user agent MAY change the request method from POST to GET for the subsequent request.
GET methods unchanged. Others may or may not be changed to GET.
12. If one of the following is true
- internalResponse’s status is 301 or 302 and request’s method is `POST`
- internalResponse’s status is 303 and request’s method is not `GET` or `HEAD`
then:
Set request’s method to `GET` and request’s body to null.
For each headerName of [request-body-header] name, delete headerName from request’s header list.
request-body-header
A request-body-header name is a header name that is a byte-case-insensitive match for one of:
`Content-Encoding`
`Content-Language`
`Content-Location`
`Content-Type`
新增以下 NodeJS 程式碼
if (req.url === "/302") {
res.statusCode = 302;
res.setHeader("location", "http://localhost:5000");
res.end();
return;
}
先用瀏覽器打開 http://localhost:5000/302 做個簡單的 GET 請求,確認 GET 請求維持不變
再來用 fetch 做個 Post + Body 的請求
fetch("http://localhost:5000/302", {
method: "POST",
body: "hello-world",
redirect: "follow",
headers: { "Content-Type": "text/plain" },
})
.then((res) => res.text())
.then((body) => console.log(body));
確實轉為 GET + 清空 Body 了
繼續試試看用 fetch 做個 PUT + Body 的請求
fetch("http://localhost:5000/302", {
method: "PUT",
body: "hello-world",
redirect: "follow",
headers: { "Content-Type": "text/plain" },
})
.then((res) => res.text())
.then((body) => console.log(body));
保持原本的 Request Method + Body
<form method="POST" action="http://localhost:5000/subscribeEDM">
後,使用 303 將用戶導到其他頁面(好像蠻合理的,但實務上我沒看過有網站這樣做)新增以下 NodeJS 程式碼
if (req.url === "/303") {
res.statusCode = 303;
res.setHeader("location", "http://localhost:5000");
res.end();
return;
}
再來用 fetch 做個 Post + Body 的請求
fetch("http://localhost:5000/303", {
method: "POST",
body: "hello-world",
redirect: "follow",
headers: { "Content-Type": "text/plain" },
})
.then((res) => res.text())
.then((body) => console.log(body));
轉導的 HTTP Request 確實變成 GET,且 Body 被捨棄
新增以下 NodeJS 程式碼
if (req.url === "/307") {
res.statusCode = 307;
res.setHeader("location", "http://localhost:5000");
res.end();
return;
}
再來用 fetch 做個 Post + Body 的請求
fetch("http://localhost:5000/307", {
method: "POST",
body: "hello-world",
redirect: "follow",
headers: { "Content-Type": "text/plain" },
})
.then((res) => res.text())
.then((body) => console.log(body));
轉導的 HTTP Request 確實變成 GET,且 Body 被捨棄
如果回傳 3xx,但沒有 Location Header,會發生什麼事情呢?
新增以下 NodeJS 程式碼
if (req.url === "/301-with-no-location") {
res.statusCode = 301;
res.end();
return;
}
用瀏覽器打開 http://localhost:5000/301-with-no-location 做個簡單的 GET 請求,沒有噴錯誤訊息,一切平安
如果回傳 200 + Location Header,會發生什麼事情呢?
新增以下 NodeJS 程式碼
if (req.url === "/200-with-location") {
res.statusCode = 200;
res.setHeader("location", "http://localhost:5000");
res.end();
return;
}
用瀏覽器打開 http://localhost:5000/200-with-location 做個簡單的 GET 請求,沒有轉導
我推測瀏覽器會去判斷特定 status code 搭配 location response header 才會轉導,參考 MDN 文件 的描述
Status responses including a Location header: 201, 301, 302, 303, 307, 308.
使用 201 看看,新增以下 NodeJS 程式碼
if (req.url === "/201") {
res.statusCode = 201;
res.setHeader("location", "http://localhost:5000");
res.end();
return;
}
用瀏覽器打開 http://localhost:5000/201 做個簡單的 GET 請求,沒有成功轉導QQ
由於 201 Created 通常用在 POST 請求新增資源,所以我們換成 POST + Null Body 試試看:
fetch("http://localhost:5000/201", {
method: "POST",
redirect: "follow",
})
.then((res) => res.text())
.then((body) => console.log(body));
還是沒有轉導
加個 Body 試試看:
fetch("http://localhost:5000/201", {
method: "POST",
body: "hello-world",
redirect: "follow",
headers: { "Content-Type": "text/plain" },
})
.then((res) => res.text())
.then((body) => console.log(body));
還是沒有轉導
為何 201 搭配 Location 沒有轉導呢?根據 MDN 文件 跟 RFC9110 的描述,都沒有特別提到 201 + Location "可以用來被轉導",RFC 針對上述 3xx status code 的用詞常常會出現
The user agent MAY use the Location field value for automatic redirection.
其中蠻有趣的,大寫的 MAY,還有專門一篇 RFC2119 來描述每個 Keyword 的意義,節錄 MAY 的部分描述:
MAY
This word, or the adjective "OPTIONAL", mean that an item is
truly optional.
結論:RFC 沒有明確禁止 HTTP Client 在 201 的時候使用 Location Header 轉導,但也沒有說 MAY use the Location field value for automatic redirection
,實務上我沒看過 201 會轉導的,在 Response Body 回傳新建立的資源是比較常見的做法
這篇文章,帶大家理解 301, 302, 303, 307, 308 redirect 的差異,也介紹了 201 這個特殊的情境,我自己也學到了很多~