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.js
const 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.js
const 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

RFC 7538


B2D1 (包邦东)

Written by B2D1 (包邦东)