HTTP之响应状态码

状态码是一个三位的数字,用来告知客户端请求的处理结果。三位数字的第一位是一个分类,指明状态码的类型,以此数字把状态码分为5类:

  • 1xx: 信息类 。
  • 2xx: 成功。请求被成功的接受或者理解,或者执行。我们常见的200 OK就是这个分类内的。
  • 3xx: 重定向 - 为完成请求,需要进一步的行动。我们常见的301 Redirect就是这个分类内的。
  • 4xx: 客户端错误。客户端提交的数据错误,不能被理解或者接受等等。我们常见的404 Not Found 就是这个分类内的。
  • 5xx: 服务器错误。错误发生了,是服务器的问题,和客户端无关。

200 型响应

200 系列的状态码都可以表示请求已经成功处理。可是作为成功的语义,细节可以各不相同。并且对 HTTP 客户端有不同的动作指示,比如是否刷新当前页面内容等。

首先看下本类型响应下的具体状态码列表:

200 OK
201 Created 
202 Accepted 
204 No Content
205 Reset Content
206 Partial Content

200 OK

可能是最常用的状态。它指明请求已经成功完成。

201 Created

201 Created 会比200 OK 有更加具体的语义。201指明请求成功且创建了一个资源,因此201常常配合PUT方法使用,因为PUT方法的语义上就是创建一个资源。

202 Accepted

202 Accepted 会比200 OK 有更加具体的语义。202表明请求成功被接受,但不一定已经完成资源创建或者修改,而只是被接受,可能还有服务器的后续的处理。

204 No Content

表明请求处理成功,但是作为服务器并不想要提供消息在消息主体内,或者并没有什么消息主体需要提供。 比如使用DELETE请求情况下,如果服务成功完成,可以返回204 No Content。此场景下就是告诉客户端:“你的DELETE请求已经完成,但是因为这个资源已经被删除,所以,也就没有什么需要返回的消息”。

204 对用户代理(浏览器)也是有意义的。用户代理收到204,就不应该引发请求的文档的当前视图(就是不要去刷新当前文档,也不要导航到别的URL)。这就意味着,如果有一个 HTML Form然后提交,如果服务器返回的状态是204状态,那么浏览器不可以刷新窗口或者到其他的页面。所有的 Form 输入的内容都不要改变。当然这样的做法在用户代理中很少有人如此实践。因为用户点击发生了,和服务器的交互也发生了,而用户界面却对此毫无响应,那么这样的做法显然会让用户感到迷惑。

可要是在ajax的应用上下文,这样做就比较有价值了。ajax应用获得204状态返回就可以提示用户操作已经成功。并且如同204状态码的意图,不需修改当前 Form 的任何值。因为服务本来不需要返回任何具体数据,它只需要告诉客户端请求已经成功处理。

205 Reset Content

此状态码告诉客户端请求已经成功执行。不同于204,它的意图是要告诉客户应该清除Form的内容或者刷新用户界面。具体说,我可以填写Form,提交后,如果接到了205响应,就应该重设Form,然后初始化一个新的输入。

事实上,并没有什么浏览器支持这样的意图:浏览器要么把205当成204,要么当成200。然而,对于ajax应用就可以实现这样的意图:ajax应用接到205码,用户界面应该把数据设置到默认值。如果是 Restful App ,在数据输入场景下,204响应适合对一条记录做一系列的编辑;205更适合输入一系列的记录;故而,Restful App的建议更加尊重http的设计本意。

206 Partical Content

它的存在目的是为了支持大文件的分段下载。当客户端发起资源范围请求,服务器就可以返回206型响应,告知客户端操作成功并且返回部分内容。见如下案例:

HEAD /large.jpg HTTP/1.1
Host: example.com

HTTP/1.1 200 OK
Accept-Ranges: bytes
Content-Length: 3980

GET  /large.jpg HTTP/1.1
Host: example.com
Range: bytes=0-999

HTTP/1.1 206 Partial Content
Accept-Ranges: bytes
Content-Length: 1000
Content-Range: bytes 0-999/3980

{binary data}

GET /large.jpg HTTP/1.1
Host: example.com
Range: bytes=1000-

HTTP/1.1 206 Partial Content
Accept-Ranges: bytes
Content-Length: 2980
Content-Range: bytes 1000-3979/3980

{binary data}

上面的案例中,首先通过HEAD 方法查询资源的大小、以及查询是否支持分段下载。服务器如果支持分段下载就通过Accept-Ranges: bytes 的首部字段指示它是支持的。接下来,客户端就可以通过首部字段 Range 来指定要获取资源的范围。而服务器通过206 Partial Content指示获取成功、本次获取为部分内容、和本次获取部分资源的范围。

300 型响应

3xx系列的响应涉及的状态码除了304 Not Modified 之外都是用于重定向的。我们首先查看3xx系列的重定向状态码和描述:

  • 300 Multiple Choices 客户端请求了实际指向多个资源的URL。
  • 301 Moved Permanently 请求的 URL 已移走。
  • 302 Found 请求的URL临时移走
  • 303 See Other 客户端应该使用指定URL
  • 307 Temporary Redirect 客户端应该临时定位到指定URL

于是,看起来并不复杂的重定向,稍微对比就会感到很混淆的。特别是301/302 ,303/307 一组,好像根本就是重复的。我们会在下文解释。

300 multiple choices

含义在于——同样的一个URL,可以对应多个实际的资源。比如同样的软件下载可以有多个平台的版本,或者多种打包压缩格式。又比如,同样的文档可以有不同的文档格式 。客户端可以在这些结果中,根据自己的情况作出自动的选择(比如中文用户就自动选择中文文档),或者给出列表,提交给最终用户选择。

可是,标准内并没有给出具体的多个选择项的格式。因此,该状态码很少被标准的web服务器和用户代理使用。我看到的两本和http有关的书,讲到状态码 300 的时候,一本语焉不详,一本干脆略过,想来也是这样的原因。

但是程序员完全可以在300状态码的基本含义情况下,具体化多资源构造的格式,在自己的应用的客户端和服务器之间遵守,然后实现应用的特定目的。比如,在实体主体内自定义如下的格式:

HTTP/1.1 300 Multiple Choices
Date: Tue, 11 Jun 1996 20:02:21 GMT
Content-Type: text/html
Content-Length: 130

<h2>Multiple Choices:</h2>
<ul>
<li><a href=paper.1>HTML</a>
<li><a href=paper.2>Kindle</a>
<li><a href=paper.3>Doc</a>
</ul>

要么提供选择界面 ,由用户选择其中一个、要么由用户代理自动选择,然后重定向到这个资源去。

301 Moved Permanently

说的是客户端请求的 URL 对应的资源已经被挪到其他位置,这个新位置已经在响应消息的LOCATION 头字段内指定。如果你的书签使用了这个URL,那么应该由用户代理自动更新到新的位置。下次访问也希望使用新的URL。

客户端请求:

GET /abc HTTP/1.1
Host: www.example.org

服务器响应:

HTTP/1.1 301 Moved Permanently
Location: http://www.example.org/def

302 Found

说的是你当前访问的URL对应的资源暂时被移动到一个新位置,这个新位置在Location头内指定。和301不同的是,302并不影响你的书签,你也不必下次访问新的URL,因为这个变化是暂时的。实际上,这个状态码在HTTP 1.0引入,本来命名就是 302 Moved Temporarily,以便和301对照使用。

客户端请求:

GET /abc HTTP/1.1
Host: www.example.org

服务器响应:

HTTP/1.1  302 Found
Location: http://www.example.org/def

然而,对于这个状态码,标准本来希望的是保持两次请求的请求方法一致的。就是说,原来用POST重定向就用POST;原来用GET方法请求的,重定向后也继续用 GET 方法。而实际上,众多的用户代理都做了和标准不一致的实现:不管原来引发请求的是POST,还是GET,在重定向后都改成了GET方法。这是不恰当的实现,但是因为大家都这么实现,故而它反而成为了事实上的标准。为此,在修订 HTTP 1.1版本时就又引入了303 See Other 和307 Temporary Redirect 状态码,以便解决标准和实现不一致引发的语义问题。请继续阅读,以便理解这两个新的、看起来有些混淆的状态码。

303 See Other

此状态码也是重定向。但是它不管之前的请求方法是什么,都强制要求转换请求方法为GET

307 Temporary Redirect

此状态码也是重定向响应。但是它和303不同,它要保持新的请求方法和之前发起请求的请求方法一致。就是说,如果之前是 GET 方法,那么这次重定向也需继续使用GET 方法;之前使用POST方法,这次依然需要保持使用POST 方法。

304 Not Modified

此状态码其实和重定向无关。但是总不至于单独为它一个而增加一个分类,所以就放到了300系列内。当用户代理发起GET请求并设置了修改时间的前条件,而服务器发现被请求的资源并没有在给出的时间后被修改,就会返回这个状态码。这个状态码的存在是为了性能上的考量。不必传递用户代理有的、服务器也没有修改的资源。案例:

客户端请求:

GET /sample.html HTTP/1.1
Host: example.com
If-Modified-Since: Wed, 01 Sep 2004 13:24:52 GMT

服务器响应:

HTTP/1.1 304 Not Modified
Date: Tue, 27 Dec 2005 05:25:19 GMT  

400 型响应

400 系列响应消息都是用来由服务器告诉客户端,收到的请求是它无法处理的。比如最常用的404 Not Found可以用来指示客户端请求的资源在服务器上根本没有。

400系列数量众多,但是大部分都比较简单。所以我只会把 412、417、403 单列出来做特别说明,因为前两个错误相对而言和协议的其他特性耦合比较多,因此显得复杂,并且对于设计良好的Restful app来说也是比较实用的。第三个状态码比较常见,但是标准定义比较抽象,我希望把它可以具体化一点。

412 Precondition Failed

客户端发起了条件请求,服务器发现这个请求中的其中一个条件并不成立,那么服务器就会用此错误码作为响应消息的状态码返回给客户端。

请求头上可以使用如下字段对请求做出条件限定:

If-Match
If-Modified-Since
If-None-Match
If-Range
If-Unmodified-Since

这些请求头被称为前条件。通过它们可以告诉服务器只有条件满足才去完成请求的执行。

举个例子。假设我们GET了一个资源,在客户端由用户修改后提交更新。那么我们肯定希望在更新资源之前首先询问服务器此资源在 GET 后到更新之前是否有改动。如果有了改动,就说明另一个用户已经修改它了,我们当前的更新就不应该执行,否则就可能会导致不一致的事务了。具体的做法就是使用 POST 方法请求去更新资源的状态,并加上 If-Unmodified-Since 限定只有你最近GET资源之后此资源没有被修改的这个条件满足才去执行更新。而服务器可以校验此条件,如果这个条件没有满足,那么返回状态码412 (Precondition Failed) 即可。

417 Expectation Failed

客户端在请求头内加入了Expect字段,这个字段要求的期望如果并不能被服务器支持,那么服务器会返回417 状态码给客户端。

案例:

GET /todo/1 HTTP/1.1
Host: example.org
Content-Type:json
Expect: 100-continue

HTTP/1.1 417 Expectation Failed

403 Fobidden

服务器禁止提供资源来响应它,尽管客户端的请求是有效的。相对而言,401 Unauthorized响应只要客户端提供授权就可以得到资源;403 fobidden 是即使客户端给了用户授权也是不行的。那么到底是什么原因禁止呢?标准在此处是沉默的。看到这个状态码,作为客户除了离开或者重试外,其实并不知道到底出了什么错,也不知道如何解决。为了希望用户不太困惑,有人为此错误给出了更加详细的子错误码——这是微软的IIS服务器的做法。这里给出了子错误码的局部列表:

403.1 - Execute access forbidden 
403.2 - Read access forbidden 
403.3 - Write access forbidden 
403.4 - SSL required. 
403.5 - SSL 128 required. 
403.6 - IP address rejected 
403.7 - Client certificate required. 
403.8 - Site access denied 
403.9 - Too many users 

对程序员来说,这些状态子码,大部分可以一眼看明白它的含义,因此这套错误码可以帮助对403 Fobidden 的本意做出更加清晰的了解。所以尽管它们并不是标准的一部分,也依然是值得去学习和了解的。 对于用户而言,仅仅看到403 fobidden 而不去了解细节或许是更好的选择。

更多的400系列的错误码是比较简单的,其中大部分都是可以望文生义的了解到它的错误场景。比如411 Length Required 就是告诉客户端你的请求必须有Content-Length首部。401 Unauthorize 则是告诉客户端当前访问的资源需要认证,请提供用户名和密码过来。这里就不详述了。

500 型响应

这个系列响应消息的状态码,对用户而言表现的更加含糊。看到了这个错可以确认的就是:这不是客户端的错,也不是用户的错。它就是服务器的错。服务器也不想让用户或用户代理知道更多的细节。

500 Internal Server Error

就是一个这样模糊的错。语义上就是服务器遇到了一个妨碍它提供服务的错误,就使用此状态码。作为服务器的开发者,应该需要在发出这个错误时,内部记录具体的、可以有助于解决问题的错误消息。

503 Service Unavailable

说明服务器现在无法提供服务,但是将来可以。如果服务器知道何时资源可用,应该在响应中包含Retry-After的首部,提示客户端可以重试服务的时间。

这个状态码略显诡异的是,既然服务已经不可用,那么这条消息是谁给出的?

状态码由Web Server给出,指示为当前网页服务的模块被关闭了。

以IIS为例。IIS是有应用池(application pool)的概念,它是一个比Web Server更小的模块,每个网页都由应用池提供具体服务。要是应用池被关闭了,那么,我们就会遇到这个错误。原因可能是:

  1. 你的应用崩溃了
  2. 或者你的应用常常崩溃,因此IIS决定关闭你的应用池。(常常崩溃的标准是5分钟内5次)

100型响应

当客户端发送 Expect:100-Continue时, 服务端可以响应 100 Continue 为允许,或者不许可(417 Expectation Failed) 。100 Continue 状态码通知客户端可以继续发送请求。

在发送大文件之前,客户端可以首先发出询问,要是在服务器不接受大文件的话,服务器就可以直接拒绝继续。否则,服务器只能从请求头内提取内容大小,当发现不符合条件的时候,实体内容这时候可能已经在传递了。这可是在浪费带宽了。

案例

以Node.js创建http服务器,客户端试图 POST 一个 MP3 视频到服务器,这个文件大小为101MB。客户端并没有在请求主体内发送这个文件,而是添加一个 Expect: 100-continue 的请求首部字段。如果可以接受这样大小的文件,服务器就返回100 Continue ,否则返回417 状态码。

使用nc 做客户端,直接在console内贴入请求消息文本,并回车两次就可以发出请求到服务器。

$ nc  localhost 8181
POST /content/videos HTTP/1.1
Host: media.example.org
Content-Type: video/mp4
Content-Length: 105910000
Authorization: Basic bWFkZTp5b3VfbG9vaw==
Expect: 100-continue

HTTP/1.1 100 Continue

HTTP/1.1 200 OK
Date: Mon, 23 Nov 2015 06:27:53 GMT
Connection: keep-alive
Transfer-Encoding: chunked

c
hello world

0

Node.js的 http 服务器默认接受任何大小的文件。因此,当它发现 Expect: 100-continue时,会返回 HTTP/1.1 100 Continue。

希望覆盖默认行为的方法,根据条件(比较具体文件大小)决定是否接受的话,可以参考 Node.js 手册:https://nodejs.org/api/http.html#http_event_continue

101 型响应

HTTP 协议提供一个机制,允许在已经建立的连接上把HTTP协议切换到一个新的、不兼容的协议上。

客户端可以发起这个协议切换请求,而服务器可以选择拒绝并关闭连接,或者选择接受。如果服务器选择了接受,接下来就可以在此连接上传递新的协议内容。这样做的好处在于不必重建连接即可做协议升级或者调整为新的协议。服务器可以发送101型响应消息给客户端表示接受协议切换。比如本来是HTTP/1.1协议可以经过101 Switch Protocols就改变为h2c 、WebSocket、TLS。握手完成后,传递的协议就此改变。

案例:如何利用101 Switch Protocols 把HTTP协议切换为WebSocket?

客户端通过http协议的 GET 方法的首部字段,向服务器发起请求:

GET ws://echo.websocket.org/?encoding=text HTTP/1.1
Origin: http://websocket.org
Cookie: __utma=99as
Connection: Upgrade
Host: echo.websocket.org
Sec-WebSocket-Key: uRovscZjNol/umbTt5uKmw==
Upgrade: websocket
Sec-WebSocket-Version: 13

特别留意的是首部的这两行:

Connection: Upgrade
Upgrade: websocket

这两个首部字段,就是指明客户端向服务器发起请求,希望把连接升级到websocket。

如果服务器端理解这个请求 ,就会返回一个响应:

HTTP/1.1 101 WebSocket Protocol Handshake
Date: Fri, 10 Feb 2012 17:38:18 GMT
Connection: Upgrade
Upgrade: WebSocket

在响应首行,只是状态码为101,就是协议切换被认可。之后再这个连接上就可以传递websocket协议了。

案例: ADDONE 协议

为了演示升级的过程,我们可以自己实现一个叫做 ADDONE的新协议。这个协议希望客户端发送一个整数过来,然后把这个数字加1后返回给客户端。在正常的http 连接内发送单一的一个整数并不符合HTTP协议的请求包规定,因此会被识别为无效数据而报错,或者被HTTP协议置之不理。但是切换协议后,同样的连接来的数据将不再被HTTP 代码解析,因此不会被HTTP协议识别为非法。ADDONE协议并不具备实用性,但是可以演示HTTP 的升级过程。

实际的代码和测试用例,都在本书附属代码的code/addone.js内。这里仅仅提到其中实现的一些要点:

具体而言,Node 在HTTP 模块会有一个upgrade事件:

function (request, socket, head) { }

它在收到客户端升级请求后发射。如果没有应用代码侦听这个事件,node会关闭此连接。

如果在此事件内确认可以升级,就把 101 Switch Protocols响应发给客户端。然后,HTTP 实现将不再侦听socket的data事件,而升级后的协议实现应该监听data事件并根据data内容做出响应。想要怎么解析请求和发送响应就是新代码的工作了。


本文参考自:《HTTP小书》(刘传君)


本文最后编辑于 2018-02-22 16:06

你可能感兴趣: