联系 Koa.js 和 requests 看 HTTP 协议中的重定向

在 HTTP 协议中,一般 3 开头的状态码,都用于表示 重定向:因为某些原因,例如目标网页已经存在其它网站,服务器会通知客户端访问另一个网页。

Location

为了告诉客户端应改前往哪一个页面,服务器在返回的响应 (response) 的 headers 中用 Location 字段标明具体应该访问的页面。

例如,访问 http://example.com 时,如果服务器想让浏览器跳转到 http://google.com,可以在 response 中写:

1
2
HTTP/1.0 302 Redirect
Location: http://google.com

一般浏览器收到后,会自动跳转。

另外,URL 也可以标明为相对路径,比如,在上个例子中,如果跳转到 http://example.com/hello.html,则可以标记为:

1
2
HTTP/1.0 302 Redirect
Location: hello.html

Koa.js

在 Koa.js 中,context 有一个方法为 redirect,专门用于定向,而这个方法实际委托给了 lib/response.js

其具体代码为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
redirect(url, alt) {
// location
if ('back' == url) url = this.ctx.get('Referrer') || alt || '/';
this.set('Location', url);

// status
if (!statuses.redirect[this.status]) this.status = 302;

// html
if (this.ctx.accepts('html')) {
url = escape(url);
this.type = 'text/html; charset=utf-8';
this.body = `Redirecting to <a href="${url}">${url}</a>.`;
return;
}

// text
this.type = 'text/plain; charset=utf-8';
this.body = `Redirecting to ${url}.`;
},

如果 URL 为 back,那么会跳转回请求来源的方向,比如你在 http://github.com/lazzzis 点击了 lazzzis.github.io,那么在请求 lazzzis.github.io 的 request 的头部中,字段为 Referrer: http://github.com/lazzzis。换句话说,back 的意思就是 “从哪里来,就回哪里去”。

this.set('Location', url) 作用则就是之前说的,将头部 headers 中 Location 设置为客户端应该去访问的那个 URL。之后,便是将状态码设置为 302。

这边,Koa.js 怕浏览器不会自动跳转,因此将也设置了消息主体部分,通知用户应该跳转。

requests

接下来用 Python 的 requests 做实验。我们先用 Koa.js 写一个简单的服务端:

1
2
3
4
5
6
7
8
9
10
11
12
const Koa = require('koa')
const koaLogger = require('koa-logger')

const app = new Koa()

app.use(koaLogger())

app.use(async (ctx, next) => {
ctx.redirect('http://lazzzis.github.io')
})

app.listen(5000)

然后发起请求:

1
2
3
4
5
import requests

r = requests.post("http://localhost:5000")
print(r.url)
# http://lazzzis.github.io

可以发现,requests 已经帮我们做了自动跳转。如果不想让它跳转的话,可以设置 allow_redirects 参数(默认为 True):

1
2
3
4
5
r = requests.post("http://localhost:5000", allow_redirects=False)
print(r.url)
# http://localhost:5000/
print(r.text)
# Redirecting to <a href="http://lazzzis.github.io">http://lazzzis.github.io</a>.

取消跳转后,可以看到它这次停止了跳转。关于限制跳转的相关源码在 requests.py (代码太长,所以就不粘贴了)。

在 653 行: yield_requests=True 使得在 resolve_redirects 中时,不会进入下一步的 send:在 206 - 225 的分支可以看到, 如果 yield_requests=True,那么 requests 会做接下来的请求。

获取下一个请求的 URL

requests.py 的 98 行的 get_redirect_target 的实现中,location = resp.headers['location'] 表明了这里的处理和 Koa.js 是一样的,也是从 location 字段获取。

更改请求方式

另外,还有一个有意思的事情,我在请求的时候,发的是 POST 请求,可是 GitHub Pages 不支持 POST 的呀,那么 requests 一定换了另一种方法:

1
2
3
r = requests.post("http://localhost:5000")
print(r.request.method)
# GET

看的出来,requests 使其变为了 GET。这里的实现在于: requests.py 的 164 行 self.rebuild_method(prepared_request, resp) 和 292 行开始的 rebuild_method 的实现。尤其是 304 行和 309 行,将请求方法改为了 GET

递归请求

这里想象两种极端情况。

一是如果服务端 A 实现出错,使得要求客户端依旧跳到 A。那么,requests 请求 A, 而之后有继续请求 A。这样,陷入了一个死循环。

第二种,类似,但不只一个服务器出错: A 要求跳转到 B,而 B 要求跳转到 C,可是 C 又要求跳转到 A,那么,这里也同样陷入了一个死循环。

requests 考虑到了这点,做了限制,避免一直跳转:

先对上面的服务端做一点修改:

1
2
3
app.use(async (ctx, next) => {
ctx.redirect('http://localhost:5000')
})
1
2
r = requests.post("http://localhost:5000")
# requests.exceptions.TooManyRedirects: Exceeded 30 redirects.

python 自定义了一个异常,用于说明引起的原因是过多的重定向,并且说明了 requests 最先跳转次数为 30 次。

可以看到在 requests.py 的第 139 行处,requests 本身记录了请求的历史,如果历史条数,也就是请求的次数,大于限制时会抛出异常。

参考资料

  1. HTTP: The Definitive Guide
  2. Redirections in HTTP