HTTP redirections
前言
一直沒有很深入了解 3xx Status Code 有哪些,各代表什麼含意,就趁這篇文章把它搞懂吧!
Permanent redirections
- 代表原本的網址已經不再使用了
- 例如,網站搬家,從 old.example.com 搬到 new.example.com
- 搜尋引擎爬蟲收到 Permanent redirections 之後,,會將轉導的 URL 記錄下來(原網站的 SEO 會指到新網站)
- 預設會被快取
- 根據 RFC7231 的建議
The server's response payload usually contains a short hypertext note with a hyperlink to the new URI(s).
301 Moved Permanently
- 轉導的 HTTP Request 統一用 GET METHOD 來做導轉,若有 Body 會被捨棄
我們使用 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 被捨棄
308 Permanent Redirect
- 轉導的 HTTP Request Method 跟 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 確實不變
Temporary redirections
- 搜尋引擎爬蟲收到 Temporary redirections 之後,不會將轉導的 URL 記錄下來(意思就是原本網站的搜尋引擎排名不會被影響)
302 Found
- 根據 RFC9110 的描述
Note: For historical reasons, a user agent MAY change the request method from POST to GET for the subsequent request.
- 根據 MDN 文件 的描述
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
303 See Other
- 轉導的 HTTP Request 統一用 GET METHOD 來做導轉,若有 Body 會被捨棄,同 301 的行為
- 使用情境: 用戶訂閱電子報
<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 被捨棄
307 Temporary Redirect
- 轉導的 HTTP Request Method 跟 Body 不變,,同 308 的行為
新增以下 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 with no Location
如果回傳 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 請求,沒有噴錯誤訊息,一切平安
non 3xx with location
如果回傳 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 這個特殊的情境,我自己也學到了很多~
參考資料
- https://developer.mozilla.org/en-US/docs/Web/HTTP/Redirections
- https://datatracker.ietf.org/doc/html/rfc7231
- https://www.rfc-editor.org/rfc/rfc9110
- https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Status/201
- https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Status/301
- https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Status/302
- https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Status/302
- https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Status/307
- https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Status/308
- https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Location