HTTP Note #2: HTTP request message method

HTTP Request Message Format

一个 request message 组成如下图:

由于上次 note 中大致了解过其组成,所以这次详细了解 method,且自己会尽力将所有的方法都亲自尝试一下。

Method token

method: 指明了在 Request-URI 指定资源上需要执行的方法,由于其对大小写敏感,所以一定要使用大写。method 有以下几种选项:

Method = "OPTIONS"
      | "GET"
      | "HEAD"
      | "POST"
      | "PUT"
      | "DELETE"
      | "TRACE"
      | "CONNECT"
      | extension-method
extension-method = token

其中:

  • OPTIONS: 用于确定 web server 所支持的所有 methods,可以指定特定的 URL,或者也可以用星号(*)代表 server的所有资源。其响应是不可缓存的。

    以下是自己写的测试代码:

    function sendRequest(method, url) {
        var xmlhttp = new XMLHttpRequest()
        xmlhttp.open(method, url, true)
        xmlhttp.send()
    }
    
    sendRequest('OPTIONS', url)
    

    一共测试了 5 个网页,并且以上代码分别在各自的访问域下运行,虽然麻烦了一点,但是避免了跨域访问的问题,或许还有更好更省力的方法?

    结果:

        已测网站:                         状态
      - http://www.ietf.org/              200 OK
      - http://www.baidu.com/             (failed) net::ERR_EMPTY_RESPONSE
      - https://github.com                422 Unprocessable Entity
      - https://www.google.com.hk         405 Method Not Allowed
      - http://shanghai.baixing.com/      404 Not Found
    

    其中,http://www.ietf.org/ 是 IETF (Internet Engineering Task Force) 的网站,由于其本身就是规定互联网相关技术,所以其网站理应是符合规范的。当然,它正确返回了状态码 200 和 server 支持的 methods(见以下 Allow 头域):

      HTTP/1.1 200 OK
      Date: Sun, 20 Apr 2014 13:22:23 GMT
      Server: Apache
      Allow: GET,HEAD,POST,OPTIONS,TRACE
      ……
    

    事实上,从测试的结果可以大概感觉到基本上没有服务器会支持该方法。当然上面的响应结果也是各不相同,这有点儿出乎我的意料,因为 RFC 2616 规定:

    An origin server SHOULD return the status code 405 (Method Not Allowed) if the method is known by the origin server but not allowed for the requested resource, and 501 (Not Implemented) if the method is unrecognized or not implemented by the origin server.

    最诡异的要数 baidu,我甚至没能把 request 发送出去,出现了 CAUTION:Provisional headers are shown 的错误,排除了 AD block 类似的屏蔽且清除了 cache 后再尝试了几遍,依旧是这个错误。根据 net::ERR_EMPTY_RESPONSE 是否可以猜测 baidu 返回了空响应?具体原因自己也不是非常清楚,暂且先放着。

  • GET:灰常灰常常用且 HTTP 服务器必须支持的方法,用于获取被请求 URI 所指定的信息,可缓存。通过以上栗子可看出,还是 IETF 官网最靠谱了,于是下面 IETF Server 有支持的方法都用其测试。

    测试代码:

    sendRequest('GET', 'http://www.ietf.org/')
    

    结果:

      HTTP/1.1 200 OK
      Date: Sun, 20 Apr 2014 14:06:34 GMT
      Server: Apache
      Last-Modified: Wed, 09 Apr 2014 17:12:51 GMT
      ETag: "8846692-49f5-4f69f35ef70bc"
      Accept-Ranges: bytes
      Vary: Accept-Encoding
      Content-Encoding: gzip
      Content-Length: 4395
      Keep-Alive: timeout=15, max=100
      Connection: Keep-Alive
      Content-Type: text/html
    

    如果请求消息包含 If-Modified-Since, If-Unmodified-Since, If-Match, If-None-Match 或者 If-Range 头域,GET 的语义将变成 “conditional GET”,其目的自然是充分利用缓存、节省带宽。但是这不仅仅靠客户端就能完成,以下援引自 Restful Web Service

    条件 HTTP GET 需要由客户端和服务器共同参与完成。服务器发送表示时,应当设置一些响应报头:Last-Modified 或 ETag。客户端重复请求一个表示时,也应当设置一些报头:If-Modified-Since 和 If-None-Match,服务器根据这些信息决定是否重新发送表示。

    很开心滴看到前面的响应中服务器返回了 Last-Modified & ETag 报头,于是决定试一把 conditional GET 中 Last-Modified 与 If-Modified-Since 的搭配使用。

    测试代码:

    var xmlhttp = new XMLHttpRequest()
    xmlhttp.open('GET', 'http://www.ietf.org/', true)
    xmlhttp.setRequestHeader("If-Modified-Since", "Sat, 01 Jan 2000 00:00:00 GMT")
    xmlhttp.send()
    

    结果:

      HTTP/1.1 200 OK
    

    当然,由于时间设置在 2000 年,所以显然在 2000 年时的缓存文件不是最新的,所以服务器返回的 HTTP 状态码是 200,并发送新页面的全部内容。

    如果我们把 If-Modified-Since 的时间设为 Last-Modified 时间的后两天,即:

      xmlhttp.setRequestHeader("If-Modified-Since", "Fri, 11 Apr 2014 17:12:51 GMT")
    

    结果:

        HTTP/1.1 304 Not Modified
    

    客户端接到之后知道现有的缓存文件是最新的,就直接把本地缓存文件显示到浏览器中。

    另一种节省带宽的方法是 partial GET,其使用了 Range 头域,由于在 RFC 2616 中的内容不是十分可操作,自己另找了一篇文章 CherryPy now handles partial GETs。由于一般 GET 返回了响应 Accept-Ranges: bytes ,所以我们知道该服务器接受 bytes,因此可以这么干:

    var xmlhttp = new XMLHttpRequest()
    xmlhttp.open('GET', 'http://www.ietf.org/', true)
    xmlhttp.setRequestHeader('RANGE', 'bytes=0-99')
    xmlhttp.setRequestHeader('Cache-Control', 'no-cache')
    xmlhttp.send()
    

    结果:

      HTTP/1.1 206 Partial Content
      ……
      Content-Range: bytes 0-99/4395
      Content-Length: 100
      ……
    

    结果真的只返回了前 100 个字节:

      <!DOCTYPE HTML PUBLIC
    
  • **HEAD:**从字面含义就可以理解 HEAD 方法被用来获取请求实体的 header 信息而无需传输实体主体。HEAD 请求响应里 HTTP 头域里的元信息应该和 GET 请求响应的保持一致。HEAD 经常被用来测试超文本链接的有效性,可访问性,和最近是否有被修改。 和 GET 一样,HEAD 的响应可缓存。

    测试代码:

    sendRequest('HEAD', 'http://www.ietf.org/')
    

    结果:

      HTTP/1.1 200 OK
      Date: Sun, 20 Apr 2014 15:04:52 GMT
      Server: Apache
      Last-Modified: Wed, 09 Apr 2014 17:12:51 GMT
      ETag: "8846692-49f5-4f69f35ef70bc"
      Accept-Ranges: bytes
      Vary: Accept-Encoding
      Content-Encoding: gzip
      Content-Length: 4395
      Keep-Alive: timeout=15, max=100
      Connection: Keep-Alive
      Content-Type: text/html
    

    和之前的 GET 方法返回的响应比较,所有的头域信息是一样的,甚至 Content-Length 的值也一样(在没有正确理解 HEAD & Content-Length 的用义之前,自己会预测其值会不同)。当然,HEAD 并没有传送实体主体(entity-body)。

TO BE CONTINUED …


拓展

怎么用 OPTIONS ?

!注:以下观点来自 The HTTP OPTIONS method and potential for self-describing RESTful APIs

虽然 OPTIONS 很少被服务器支持,但是并不意味着其没有什么太大用途,比如 API 就可以好好利用 OPTIONS 方法。作为一个 API server,接收到 OPTIONS 请求时,不仅需要正确返回 200 status code 和 Allow 头域,还可以返回一个 JSON 实例的实体:

作者举了一个 github 的例子:

a request like OPTIONS /repos/:user/:repo/issues should respond with a body like…

{
  "POST": {
    "description": "Create an issue",
    "parameters": {
      "title": {
        "type": "string"
        "description": "Issue title.",
        "required": true
      },
      "body": {
        "type": "string",
        "description": "Issue body.",
      },
      "assignee": {
        "type": "string",
        "description" "Login for the user that this issue should be assigned to."
      },
      "milestone": {
        "type": "number",
        "description": "Milestone to associate this issue with."
      },
      "labels": {
        "type": "array/string"
        "description": "Labels to associate with this issue."
      }
    },
    "example": {
      "title": "Found a bug",
      "body": "I'm having a problem with this.",
      "assignee": "octocat",
      "milestone": 1,
      "labels": [
        "Label1",
        "Label2"
      ]
    }
  }
}

自己测试了一下,发现只返回了 404 Not Found,也许是自己的 url 设置的不对?


感想

  • 现在从 Mou 转战到 stackedit,虽然是在线的 MD 编辑器,但是可以同步到 Google Drive & Dropbox,而且还可以发布到很多平台,当然其中对自己来说最重要的是 Gist。最后一根稻草其实是 Mou 的实时预览,虽然很直观,但是刷新导致不停的重置到顶部、重载图片之类着实会让自己怒值上升,而这点 stackedit 就做的很赞。

问题

Q: 对于任意的 HTTP method,server 都会正常响应么? A: 自问自答: 否,HTTP 服务器至少必须实现 GET 和 HEAD 方法,其他方法都是可选的。且不按照语义规范返回状态码也是常有的。

计划

  • 接下来会详细看其他 HTTP Method。由于除了常用的 POST、必须被支持的 GET & HEAD 之外的方法大多数服务器是很少支持的,所以找到支持这些方法的服务器进行测试估计也是要费点儿时间的。