HTTP 之重定向
June 06, 2020
前言
回想每次面试时,面试官总会问:“你知道临时重定向和永久重定向分别对应哪个状态码?”
而你轻车熟路得答道:“302 和 301”
Oh No,这个回答简直烂大街,何不以另一种方式来解答。
在 web 中,重定向(Redirect)的应用十分广泛,例如当用户访问失效的资源时进行跳转,用户输入 http://baidu.com/ 时跳转至 https://www.baidu.com/.
注意!这里的重定向逻辑并不发生在 JS 层面,而是指 HTTP 层面,所以需要在服务端(Node)或者 Nginx 上进行设置,即指定关键的 响应首部字段 Location 和 状态码 Status Code.
重定向有两种存在形式:
- 临时重定向(Temporary Redirect),每次重定向不影响下一次访问
- 永久重定向(Permanent Redirect),在第一次重定向后,除非用户主动清除缓存,否则后续访问都命中第一次重定向的结果,即使服务端代码发生更改
听起来可能有点绕,下面通过实践进行讲解。
“暗藏 bug”的重定向:301、302
建立以下目录:
bash
├── A.html├── B.html└── app.js
bash
├── A.html├── B.html└── app.js
js
// app.jsconst http = require("http");const fs = require("fs");const server = http.createServer((req, res) => {if (req.url === "/a") {res.statusCode = 302;res.setHeader("location", "http://localhost:3000/b");res.end();// res.end(fs.readFileSync('./A.html'));}if (req.url === "/b") {res.end(fs.readFileSync("./B.html"));}});server.listen(3000, () => {console.log("server start!");});
js
// app.jsconst http = require("http");const fs = require("fs");const server = http.createServer((req, res) => {if (req.url === "/a") {res.statusCode = 302;res.setHeader("location", "http://localhost:3000/b");res.end();// res.end(fs.readFileSync('./A.html'));}if (req.url === "/b") {res.end(fs.readFileSync("./B.html"));}});server.listen(3000, () => {console.log("server start!");});
html
<!-- A.html --><!DOCTYPE html><html lang="en"><head><meta charset="UTF-8" /><meta name="viewport" content="width=device-width, initial-scale=1.0" /><title>Document</title></head><body><h2>I'm page A</h2></body></html>
html
<!-- A.html --><!DOCTYPE html><html lang="en"><head><meta charset="UTF-8" /><meta name="viewport" content="width=device-width, initial-scale=1.0" /><title>Document</title></head><body><h2>I'm page A</h2></body></html>
html
<!-- B.html --><!DOCTYPE html><html lang="en"><head><meta charset="UTF-8" /><meta name="viewport" content="width=device-width, initial-scale=1.0" /><title>Document</title></head><body><h2>I'm page B that redirects from page A</h2></body></html>
html
<!-- B.html --><!DOCTYPE html><html lang="en"><head><meta charset="UTF-8" /><meta name="viewport" content="width=device-width, initial-scale=1.0" /><title>Document</title></head><body><h2>I'm page B that redirects from page A</h2></body></html>
首先测试 302 临时重定向,运行 app.js,访问 http://localhost:3000/a ,如下所示:
状态码为 302 Found.
修改 app.js:
js
const server = http.createServer((req, res) => {if (req.url === "/a") {// res.statusCode = 302;// res.setHeader("location", "http://localhost:3000/b");// res.end();res.end(fs.readFileSync("./A.html"));}if (req.url === "/b") {res.end(fs.readFileSync("./B.html"));}});
js
const server = http.createServer((req, res) => {if (req.url === "/a") {// res.statusCode = 302;// res.setHeader("location", "http://localhost:3000/b");// res.end();res.end(fs.readFileSync("./A.html"));}if (req.url === "/b") {res.end(fs.readFileSync("./B.html"));}});
再次访问 http://localhost:3000/a ,浏览器显示 A 页面,证明了重定向是临时的,下一次访问和上一次访问不存在联系。
再来测试 301 永久重定向,修改 app.js
js
const server = http.createServer((req, res) => {if (req.url === "/a") {res.statusCode = 301;res.setHeader("location", "http://localhost:3000/b");res.end();// res.end(fs.readFileSync("./A.html"));}if (req.url === "/b") {res.end(fs.readFileSync("./B.html"));}});
js
const server = http.createServer((req, res) => {if (req.url === "/a") {res.statusCode = 301;res.setHeader("location", "http://localhost:3000/b");res.end();// res.end(fs.readFileSync("./A.html"));}if (req.url === "/b") {res.end(fs.readFileSync("./B.html"));}});
访问 http://localhost:3000/a ,如下所示:
状态码为 301 Moved Permanently.
修改 app.js
js
const server = http.createServer((req, res) => {if (req.url === "/a") {// res.statusCode = 301;// res.setHeader("location", "http://localhost:3000/b");// res.end();res.end(fs.readFileSync("./A.html"));}if (req.url === "/b") {res.end(fs.readFileSync("./B.html"));}});
js
const server = http.createServer((req, res) => {if (req.url === "/a") {// res.statusCode = 301;// res.setHeader("location", "http://localhost:3000/b");// res.end();res.end(fs.readFileSync("./A.html"));}if (req.url === "/b") {res.end(fs.readFileSync("./B.html"));}});
再次访问 http://localhost:3000/a ,发现浏览器依旧显示 B 页面。
注意多了一个 from disk cache,这表明重定向结果已缓存至磁盘中,更改服务端代码也不能避免重定向。
当清除缓存后,永久重定向便会失效。
不过,好像并没有发现什么问题,这两个状态码都如预期的表现一致。
还是太年轻了,看接下来的实践。
新增 post.html
html
<!DOCTYPE html><html lang="en"><head><meta charset="UTF-8" /><meta name="viewport" content="width=device-width, initial-scale=1.0" /><title>Document</title></head><body><button>send POST request</button><script>const btn = document.querySelector("button");btn.onclick = () => {fetch("http://localhost:3000/a", { method: "post" }).then(res => res.text()).then(data => console.log(data));};</script></body></html>
html
<!DOCTYPE html><html lang="en"><head><meta charset="UTF-8" /><meta name="viewport" content="width=device-width, initial-scale=1.0" /><title>Document</title></head><body><button>send POST request</button><script>const btn = document.querySelector("button");btn.onclick = () => {fetch("http://localhost:3000/a", { method: "post" }).then(res => res.text()).then(data => console.log(data));};</script></body></html>
修改 app.js
js
const server = http.createServer((req, res) => {if (req.url === "/") {res.end(fs.readFileSync("./post.html"));}if (req.url === "/a" && req.method === "POST") {res.statusCode = 302;res.setHeader("location", "http://localhost:3000/b");res.end();}if (req.url === "/b" && req.method === "POST") {res.end("data redirects from b");}if (req.url === "/b") {res.end("what's wrong");}});
js
const server = http.createServer((req, res) => {if (req.url === "/") {res.end(fs.readFileSync("./post.html"));}if (req.url === "/a" && req.method === "POST") {res.statusCode = 302;res.setHeader("location", "http://localhost:3000/b");res.end();}if (req.url === "/b" && req.method === "POST") {res.end("data redirects from b");}if (req.url === "/b") {res.end("what's wrong");}});
这段代码表示,当我访问 http://localhost:3000/ ,点击 button,会向 http://localhost:3000/a 发送 POST 请求,而由于 /a 设置了重定向,将请求转发到 /b,所以我期望浏览器打印 data redirects from b
.
但结果如下所示:
我明明发送的是 POST 请求,但由于 302 重定向的原因,被浏览器自动修改为 GET 请求,尽管标准要求浏览器在收到该响应并进行重定向时不应该修改 HTTP Method,但是有一些浏览器可能会有问题,从而引发 Bug 🐛
“实至名归”的重定向:307、308
一般来说,HTTP 的原因短语(Reason-Phrase)能通过寥寥几个的英文单词来准确描述状态码,而 301 和 302 的描述分别是 Moved Permanently 和 Found,没有 redirect(重定向)也没有 temporary(暂时的)等字眼出现。
最重要的是,它们允许浏览器将 请求方法从 POST 更改为 GET。
所以最好是在应对 GET 或 HEAD 方法时使用 301、302,其他情况(如 POST)使用:
- 308(Permanent Redirect)替代 301
- 307(Temporary Redirect)替代 302
修改 app.js
js
if (req.url === "/a" && req.method === "POST") {res.statusCode = 307;res.setHeader("location", "http://localhost:3000/b");res.end();}
js
if (req.url === "/a" && req.method === "POST") {res.statusCode = 307;res.setHeader("location", "http://localhost:3000/b");res.end();}
重新点击 button,结果如预期所示:
总结
Permanent | Temporary | |
---|---|---|
允许将请求方法从 POST 更改为 GET | 301 | 302 |
不允许将请求方法从 POST 更改为 GET | 308 | 307 |