Skip to content
No description, website, or topics provided.
Branch: master
Clone or download
Fetching latest commit…
Cannot retrieve the latest commit at this time.
Permalink
Type Name Latest commit message Commit time
Failed to load latest commit information.
.gitignore
LICENSE.md
README.md
SUPPLEMENT.md

README.md

HTTP 接口设计指北

  • 文档主要目的是为设计接口时提供建议,使大家不必重复造 HTTP 协议已经完成的轮子
  • 只是建议,不是必须遵从的要求
  • 大家有什么问题想法或者建议欢迎 创建 Issue 或者 提交 Pull Request

目录

HTTP 协议

HTTP/1.1

2014 年 6 月的时候 IETF 已经正式的废弃了 RFC 2616 ,将它拆分为六个单独的协议说明,并重点对原来语义模糊的部分进行了解释:

相关资料:

HTTP/2

HTTP 协议的 2.0 版本还没有正式发布,但目前已经基本稳定下来了。

2.0 版本的设计目标是尽量在使用层面上保持与 1.1 版本的兼容,所以,虽然数据交换的格式发生了变化,但语义基本全部被保留下来了

因此,作为使用者而言,我们并不需要为了支持 2.0 而大幅修改代码。

URL

HOST 地址:

https://api.example.com

所有 URI 都需要遵循 RFC 3986 的要求。

强烈建议 API 部署 SSL 证书,这样接口传递的数据的安全性才能都得一定的保障。

空字段

接口遵循“输入宽容,输出严格”原则,输出的数据结构中空字段的值一律为 null

国际化

语言标签

RFC 5646 (BCP 47) 规定的语言标签的格式如下:

language-script-region-variant-extension-privateuse
  1. language:这部分使用的是 ISO 639-1, ISO 639-2, ISO 639-3, ISO 639-5 中定义的语言代码,必填
    • 这个部分由 primary-extlang 两个部分构成
    • primary 部分使用 ISO 639-1, ISO 639-2, ISO 639-3, ISO 639-5 中定义的语言代码,优先使用 ISO 639-1 中定义的条目,比如汉语 zh
    • extlang 部分是在某些历史性的兼容性的原因,在需要非常细致地区别 primary 语言的时候使用,使用 ISO 639-3 中定义的三个字母的代码,比如普通话 cmn
    • 虽然 language 可以只写 extlang 省略 primary 部分,但出于兼容性的考虑,还是建议加上 primary 部分
  2. script: 这部分使用的是 ISO 15924 (Wikipedia) 中定义的语言代码,比如简体汉字是 zh-Hans ,繁体汉字是 zh-Hant
  3. region: 这部分使用的是 ISO 3166-1 (Wikipedia) 中定义的地理区域代码,比如 zh-Hans-CN 就是中国大陆使用的简体中文。
  4. variant: 用来表示 extlang 的定义里没有包含的方言,具体的使用方法可以参考 RFC 5646
  5. extension: 用来为自己的应用做一些语言上的额外的扩展,具体的使用方法可以参考 RFC 5646
  6. privateuse: 用来表示私有协议中约定的一些语言上的区别,具体的使用方法可以参考 RFC 5646

其中只有 language 部分是必须的,其他部分都是可选的;不过为了便于编写程序,建议设计接口时约定语言标签的结构,比如统一使用 language-script-region 的形式( zh-Hans-CN, zh-Hant-HK 等等)。

语言标签是大小写不敏感的,但按照惯例,建议 script 部分首字母大写, region 部分全部大写,其余部分全部小写。

有一点需要注意,任何合法的标签都必须经过 IANA 的认证,已通过认证的标签可以在这个网页查到。此外,网上还有一个非官方的标签搜索引擎

相关资料:

时区

客户端请求服务器时,如果对时间有特殊要求(如某段时间每天的统计信息),则可以参考 IETF 相关草案 增加请求头 Timezone

Timezone: 2016-11-06 23:55:52+08:00;;Asia/Shanghai

具体格式说明:

Timezone: RFC3339 约定的时间格式;POSIX 1003.1 约定的时区字符串;tz datebase 里的时区名称

客户端最好提供所有字段,如果没有办法提供,则应该使用空字符串

如果客户端请求时没有指定相应的时区,则服务端默认使用最后一次已知时区或者 UTC 时间返回相应数据。

PS 考虑到存在夏时制这种东西,所以不推荐客户端在请求时使用 Offset 。

相关资料:

时间格式

时间格式遵循 ISO 8601(Wikipedia) 建议的格式:

  • 日期 2014-07-09
  • 时间 14:31:22+0800
  • 具体时间 2007-11-06T16:34:41Z
  • 持续时间 P1Y3M5DT6H7M30S (表示在一年三个月五天六小时七分三十秒内)
  • 时间区间 2007-03-01T13:00:00Z/2008-05-11T15:30:00Z2007-03-01T13:00:00Z/P1Y2M10DT2H30MP1Y2M10DT2H30M/2008-05-11T15:30:00Z
  • 重复时间 R3/2004-05-06T13:00:00+08/P0Y6M5DT3H0M0S (表示从2004年5月6日北京时间下午1点起,在半年零5天3小时内,重复3次)

相关资料:

货币名称

货币名称可以参考 ISO 4217(Wikipedia) 中的约定,标准为货币名称规定了三个字母的货币代码,其中的前两个字母是 ISO 3166-1(Wikipedia) 中定义的双字母国家代码,第三个字母通常是货币的首字母。在货币上使用这些代码消除了货币名称(比如 dollar )或符号(比如 $ )的歧义。

相关资料:

  • 《RESTful Web Services Cookbook 中文版》 3.9 节《如何在表述中使用可移植的数据格式》

请求方法

  • 如果请求头中存在 X-HTTP-Method-Override 或参数中存在 _method(拥有更高权重),且值为 GET, POST, PUT, DELETE, PATCH, OPTIONS, HEAD 之一,则视作相应的请求方式进行处理
  • GET, DELETE, HEAD 方法,参数风格为标准的 GET 风格的参数,如 url?a=1&b=2
  • POST, PUT, PATCH, OPTIONS 方法
    • 默认情况下请求实体会被视作标准 json 字符串进行处理,当然,依旧推荐设置头信息的 Content-Typeapplication/json
    • 在一些特殊接口中(会在文档中说明),可能允许 Content-Typeapplication/x-www-form-urlencoded 或者 multipart/form-data ,此时请求实体会被视作标准 POST 风格的参数进行处理

关于方法语义的说明:

  • OPTIONS 用于获取资源支持的所有 HTTP 方法
  • HEAD 用于只获取请求某个资源返回的头信息
  • GET 用于从服务器获取某个资源的信息
    • 完成请求后返回状态码 200 OK
    • 完成请求后需要返回被请求的资源详细信息
  • POST 用于创建新资源
    • 创建完成后返回状态码 201 Created
    • 完成请求后需要返回被创建的资源详细信息
  • PUT 用于完整的替换资源或者创建指定身份的资源,比如创建 id 为 123 的某个资源
    • 如果是创建了资源,则返回 201 Created
    • 如果是替换了资源,则返回 200 OK
    • 完成请求后需要返回被修改的资源详细信息
  • PATCH 用于局部更新资源
    • 完成请求后返回状态码 200 OK
    • 完成请求后需要返回被修改的资源详细信息
  • DELETE 用于删除某个资源
    • 完成请求后返回状态码 204 No Content

相关资料:

状态码

请求成功

  • 200 OK : 请求执行成功并返回相应数据,如 GET 成功
  • 201 Created : 对象创建成功并返回相应资源数据,如 POST 成功;创建完成后响应头中应该携带头标 Location ,指向新建资源的地址
  • 202 Accepted : 接受请求,但无法立即完成创建行为,比如其中涉及到一个需要花费若干小时才能完成的任务。返回的实体中应该包含当前状态的信息,以及指向处理状态监视器或状态预测的指针,以便客户端能够获取最新状态。
  • 204 No Content : 请求执行成功,不返回相应资源数据,如 PATCHDELETE 成功

重定向

重定向的新地址都需要在响应头 Location 中返回

  • 301 Moved Permanently : 被请求的资源已永久移动到新位置
  • 302 Found : 请求的资源现在临时从不同的 URI 响应请求
  • 303 See Other : 对应当前请求的响应可以在另一个 URI 上被找到,客户端应该使用 GET 方法进行请求。比如在创建已经被创建的资源时,可以返回 303
  • 307 Temporary Redirect : 对应当前请求的响应可以在另一个 URI 上被找到,客户端应该保持原有的请求方法进行请求

条件请求

  • 304 Not Modified : 资源自从上次请求后没有再次发生变化,主要使用场景在于实现数据缓存
  • 409 Conflict : 请求操作和资源的当前状态存在冲突。主要使用场景在于实现并发控制
  • 412 Precondition Failed : 服务器在验证在请求的头字段中给出先决条件时,没能满足其中的一个或多个。主要使用场景在于实现并发控制

客户端错误

  • 400 Bad Request : 请求体包含语法错误
  • 401 Unauthorized : 需要验证用户身份,如果服务器就算是身份验证后也不允许客户访问资源,应该响应 403 Forbidden 。如果请求里有 Authorization 头,那么必须返回一个 WWW-Authenticate
  • 403 Forbidden : 服务器拒绝执行
  • 404 Not Found : 找不到目标资源
  • 405 Method Not Allowed : 不允许执行目标方法,响应中应该带有 Allow 头,内容为对该资源有效的 HTTP 方法
  • 406 Not Acceptable : 服务器不支持客户端请求的内容格式,但响应里会包含服务端能够给出的格式的数据,并在 Content-Type 中声明格式名称
  • 410 Gone : 被请求的资源已被删除,只有在确定了这种情况是永久性的时候才可以使用,否则建议使用 404 Not Found
  • 413 Payload Too Large : POST 或者 PUT 请求的消息实体过大
  • 415 Unsupported Media Type : 服务器不支持请求中提交的数据的格式
  • 422 Unprocessable Entity : 请求格式正确,但是由于含有语义错误,无法响应
  • 428 Precondition Required : 要求先决条件,如果想要请求能成功必须满足一些预设的条件

服务端错误

  • 500 Internal Server Error : 服务器遇到了一个未曾预料的状况,导致了它无法完成对请求的处理。
  • 501 Not Implemented : 服务器不支持当前请求所需要的某个功能。
  • 502 Bad Gateway : 作为网关或者代理工作的服务器尝试执行请求时,从上游服务器接收到无效的响应。
  • 503 Service Unavailable : 由于临时的服务器维护或者过载,服务器当前无法处理请求。这个状况是临时的,并且将在一段时间以后恢复。如果能够预计延迟时间,那么响应中可以包含一个 Retry-After 头用以标明这个延迟时间(内容可以为数字,单位为秒;或者是一个 HTTP 协议指定的时间格式)。如果没有给出这个 Retry-After 信息,那么客户端应当以处理 500 响应的方式处理它。

501405 的区别是:405 是表示服务端不允许客户端这么做,501 是表示客户端或许可以这么做,但服务端还没有实现这个功能

相关资料:

错误处理

在调用接口的过程中,可能出现下列几种错误情况:

  • 服务器维护中,503 状态码

    HTTP/1.1 503 Service Unavailable
    Retry-After: 3600
    Content-Length: 41
    
    {"message": "Service In the maintenance"}
  • 发送了无法转化的请求体,400 状态码

    HTTP/1.1 400 Bad Request
    Content-Length: 35
    
    {"message": "Problems parsing JSON"}
  • 服务到期(比如付费的增值服务等), 403 状态码

    HTTP/1.1 403 Forbidden
    Content-Length: 29
    
    {"message": "Service expired"}
  • 因为某些原因不允许访问(比如被 ban ),403 状态码

    HTTP/1.1 403 Forbidden
    Content-Length: 29
    
    {"message": "Account blocked"}
  • 权限不够,403 状态码

    HTTP/1.1 403 Forbidden
    Content-Length: 31
    
    {"message": "Permission denied"}
  • 需要修改的资源不存在, 404 状态码

    HTTP/1.1 404 Not Found
    Content-Length: 32
    
    {"message": "Resource not found"}
  • 缺少了必要的头信息,428 状态码

    HTTP/1.1 428 Precondition Required
    Content-Length: 35
    
    {"message": "Header User-Agent is required"}
  • 发送了非法的资源,422 状态码

    HTTP/1.1 422 Unprocessable Entity
    Content-Length: 149
    
    {
      "message": "Validation Failed",
      "errors": [
        {
          "resource": "Issue",
          "field": "title",
          "code": "required"
        }
      ]
    }

所有的 error 哈希表都有 resource, field, code 字段,以便于定位错误,code 字段则用于表示错误类型:

  • invalid: 某个字段的值非法,接口文档中会提供相应的信息
  • required: 缺失某个必须的字段
  • not_exist: 说明某个字段的值代表的资源不存在
  • already_exist: 发送的资源中的某个字段的值和服务器中已有的某个资源冲突,常见于某些值全局唯一的字段,比如 @ 用的用户名(这个错误我有纠结,因为其实有 409 状态码可以表示,但是在修改某个资源时,很一般显然请求中不止是一种错误,如果是 409 的话,多种错误的场景就不合适了)

身份验证

部分接口需要通过某种身份验证方式才能请求成功(这些接口应该在文档中标注出来),合适的身份验证解决方案目前有两种:

超文本驱动和资源发现

REST 服务的要求之一就是超文本驱动,客户端不再需要将某些接口的 URI 硬编码在代码中,唯一需要存储的只是 API 的 HOST 地址,能够非常有效的降低客户端与服务端之间的耦合,服务端对 URI 的任何改动都不会影响到客户端的稳定。

目前有几种方案试图实现这个效果:

目前所知的方案都实现了发现资源的功能,服务端同时需要实现 OPTIONS 方法,并在响应中携带 Allow 头来告知客户端当前拥有的操作权限。

分页

请求某个资源集合时,可以通过指定 count 参数来指定每页的资源数量,通过 page 参数指定页码,或根据需求使用 last_cursor 参数指定上一页最后一个资源的标识符替代 page 参数。

如果没有传递 count 参数或者 count 参数的值为空,则使用默认值,建议在设计时设置一个最大值。

分页的相关信息可以包含在 Link HeaderX-Pagination-Info 中( HTTP 头的语法格式可以参考 ABNF List Extension: #rule )。

如果是第一页或者是最后一页时,不返回 previousnext 的 Link 。

HTTP/1.1 200 OK
X-Pagination-Info: count="542"
Link: <http://api.example.com/#{RESOURCE_URI}?last_cursor=&count=100>; rel="first",
      <http://api.example.com/#{RESOURCE_URI}?last_cursor=200&count=100>; rel="last",
      <http://api.example.com/#{RESOURCE_URI}?last_cursor=90&count=100>; rel="previous",
      <http://api.example.com/#{RESOURCE_URI}?last_cursor=120&count=100>; rel="next",
      <http://api.example.com/#{RESOURCE_URI}?last_cursor={last_cursor}&count={count}>; rel="url-template:pagination"

[
  ...
]

相关资料:

数据缓存

大部分接口应该在响应头中携带 Last-Modified, ETag, Vary, Date 信息,客户端可以在随后请求这些资源的时候,在请求头中使用 If-Modified-Since, If-None-Match 等请求头来确认资源是否经过修改。

如果资源没有进行过修改,那么就可以响应 304 Not Modified 并且不在响应实体中返回任何内容。

$ curl -i http://api.example.com/#{RESOURCE_URI}
HTTP/1.1 200 OK
Cache-Control: public, max-age=60
Date: Thu, 05 Jul 2012 15:31:30 GMT
Vary: Accept, Authorization
ETag: "644b5b0155e6404a9cc4bd9d8b1ae730"
Last-Modified: Thu, 05 Jul 2012 15:31:30 GMT

Content
$ curl -i http://api.example.com/#{RESOURCE_URI} -H "If-Modified-Since: Thu, 05 Jul 2012 15:31:30 GMT"
HTTP/1.1 304 Not Modified
Cache-Control: public, max-age=60
Date: Thu, 05 Jul 2012 15:31:45 GMT
Vary: Accept, Authorization
Last-Modified: Thu, 05 Jul 2012 15:31:30 GMT
$ curl -i http://api.example.com/#{RESOURCE_URI} -H 'If-None-Match: "644b5b0155e6404a9cc4bd9d8b1ae730"'
HTTP/1.1 304 Not Modified
Cache-Control: public, max-age=60
Date: Thu, 05 Jul 2012 15:31:55 GMT
Vary: Accept, Authorization
ETag: "644b5b0155e6404a9cc4bd9d8b1ae730"
Last-Modified: Thu, 05 Jul 2012 15:31:30 GMT

相关资料:

并发控制

不严谨的实现,或者缺少并发控制的 PUTPATCH 请求可能导致 “更新丢失”。这个时候可以使用 Last-Modified 和/或 ETag 头来实现条件请求,支持乐观并发控制。

下文只考虑使用 PUTPATCH 方法更新资源的情况。

  • 客户端发起的请求如果没有包含 If-Unmodified-Since 或者 If-Match 头,那就返回状态码 403 Forbidden ,在响应正文中解释为何返回该状态码
  • 客户端发起的请求提供的 If-Unmodified-Since 或者 If-Match 头与服务器记录的实际修改时间或 ETag 值不匹配的时候,返回状态码 412 Precondition Failed
  • 客户端发起的请求提供的 If-Unmodified-Since 或者 If-Match 头与服务器记录的实际修改时间或 ETag 的历史值匹配,但资源已经被修改过的时候,返回状态码 409 Conflict
  • 客户端发起的请求提供的条件符合实际值,那就更新资源,响应 200 OK 或者 204 No Content ,并且包含更新过的 Last-Modified 和/或 ETag 头,同时包含 Content-Location 头,其值为更新后的资源 URI

相关资料:

跨域

CORS

接口支持“跨域资源共享”(Cross Origin Resource Sharing, CORS)这里这里这份中文资料有一些指导性的资料。

简单示例:

$ curl -i https://api.example.com -H "Origin: http://example.com"
HTTP/1.1 302 Found
$ curl -i https://api.example.com -H "Origin: http://example.com"
HTTP/1.1 302 Found
Access-Control-Allow-Origin: *
Access-Control-Expose-Headers: ETag, Link, X-Total-Count
Access-Control-Allow-Credentials: true

预检请求的响应示例:

$ curl -i https://api.example.com -H "Origin: http://example.com" -X OPTIONS
HTTP/1.1 302 Found
Access-Control-Allow-Origin: *
Access-Control-Allow-Headers: Authorization, Content-Type, If-Match, If-Modified-Since, If-None-Match, If-Unmodified-Since, X-Requested-With
Access-Control-Allow-Methods: GET, POST, PATCH, PUT, DELETE
Access-Control-Expose-Headers: ETag, Link, X-Total-Count
Access-Control-Max-Age: 86400
Access-Control-Allow-Credentials: true

JSON-P

如果在任何 GET 请求中带有参数 callback ,且值为非空字符串,那么接口将返回如下格式的数据

$ curl http://api.example.com/#{RESOURCE_URI}?callback=foo
foo({
  "meta": {
    "status": 200,
    "X-Total-Count": 542,
    "Link": [
      {"href": "http://api.example.com/#{RESOURCE_URI}?cursor=0&count=100", "rel": "first"},
      {"href": "http://api.example.com/#{RESOURCE_URI}?cursor=90&count=100", "rel": "prev"},
      {"href": "http://api.example.com/#{RESOURCE_URI}?cursor=120&count=100", "rel": "next"},
      {"href": "http://api.example.com/#{RESOURCE_URI}?cursor=200&count=100", "rel": "last"}
    ]
  },
  "data": // data
})

其他资料

更细节的接口设计指南

这里还有一些其他参考资料:

You can’t perform that action at this time.