文档主要目的是为大家在设计接口时提供建议,给大家参考 HTTP 或者其他协议/指南已经设计过的内容
只是建议,不是必须遵从的要求
2014 年 6 月的时候 IETF 已经正式的废弃了 RFC 2616 ,将它拆分为六个单独的协议说明,并重点对原来语义模糊的部分进行了解释:
相关资料:
HTTP 协议的 2.0 版本还没有正式发布,但目前已经基本稳定下来了。
2.0 版本的设计目标是尽量在使用层面上保持与 1.1 版本的兼容,所以,虽然数据交换的格式发生了变化,但语义基本全部被保留下来了。
因此,作为使用者而言,我们并不需要为了支持 2.0 而大幅修改代码。
URL 的设计都需要遵守 RFC 3986 的的规范。
URL 的长度,在 HTTP/1.1: Message Syntax and Routing(RFC 7230) 的 3.1.1 小节中有说明,本身不限制长度。但是在实践中,服务器和客户端本身会施加限制*,因此需要根据自己的场景和需求做对应的调整
强烈建议 API 部署 SSL 证书,这样接口传递的数据的安全性才能获得一定的保障。
接口遵循“输入宽容,输出严格”原则,输出的数据结构中空字段的值一律为 null
RFC 5646 (BCP 47) 规定的语言标签的格式如下:
- language-script-region-variant-extension-privateuse
-
其中只有 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) 建议的格式:
相关资料:
货币名称可以参考 ISO 4217(Wikipedia) 中的约定,标准为货币名称规定了三个字母的货币代码,其中的前两个字母是 ISO 3166-1(Wikipedia) 中定义的双字母国家代码,第三个字母通常是货币的首字母。在货币上使用这些代码消除了货币名称(比如 dollar )或符号(比如 $ )的歧义。
相关资料:
关于方法语义的说明:
相关资料:
重定向的新地址都需要在响应头 Location 中返回
501 与 405 的区别是:405 是表示服务端不允许客户端这么做,501 是表示客户端或许可以这么做,但服务端还没有实现这个功能
相关资料:
部分接口需要通过某种身份验证方式才能请求成功(这些接口应该在文档中标注出来),合适的身份验证解决方案目前有两种:
REST 服务的要求之一就是超文本驱动(link:http://roy.gbiv.com/untangled/2008/rest-apis-must-be-hypertext-driven),客户端不再需要将某些接口的 URI 硬编码在代码中,唯一需要存储的只是 API 的 HOST 地址,能够非常有效的降低客户端与服务端之间的耦合,服务端对 URI 的任何改动都不会影响到客户端的稳定。
目前有几种方案试图实现这个效果:
目前所知的方案都实现了发现资源的功能,服务端同时需要实现 OPTIONS 方法,并在响应中携带 Allow 头来告知客户端当前拥有的操作权限。
大部分接口应该在响应头中携带 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
相关资料:
不严谨的实现,或者缺少并发控制的 PUT 和 PATCH 请求可能导致 “更新丢失”。这个时候可以使用 Last-Modified 和/或 ETag 头来实现条件请求,支持乐观并发控制。
下文只考虑使用 PUT 和 PATCH 方法更新资源的情况。
相关资料:
接口支持“跨域资源共享”(Cross Origin Resource Sharing, CORS)(link:http://www.w3.org/TR/cors),这里和这里和这份中文资料(link:http://newhtml.net/using-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
如果在任何 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
- })
这里还有一些其他参考资料:
这算是阅读规范的预备知识吧,但写在 README 里还是太占空间了,所以写在了这里。
规范里类似:
- header-field = field-name ":" OWS field-value OWS
-
- field-name = token
- field-value = *( field-content / obs-fold )
- field-content = field-vchar [ 1*( SP / HTAB ) field-vchar ]
- field-vchar = VCHAR / obs-text
-
- obs-fold = CRLF 1*( SP / HTAB )
- ; obsolete line folding
- ; see Section 3.2.4
格式的内容叫做“扩充巴科斯范式”,是由 RFC 5234 (Wikipedia) 定义用以描述一些内容的详细格式的定义语言。
请求头中的 User-Agent 可以帮助服务端收集设备信息,但格式需要遵循 RFC 7231 中的定义,下文是一些建议格式:
- iOS/iOS版本号 (设备型号; 是否越狱<unjailbroken, jailbroken>; 网络类型<Wi-Fi, Cellular, Unknown>; 语言) CFBundleIdentifier/CFBundleVersion
-
- Android/Android版本号 (设备型号; ROM版本号; 是否root<unrooted, rooted>; 网络类型; 语言) PackageName/PackageVersion
-
示例:
- User-Agent: iOS/6.1.2 (iPhone 5; jailbroken; Wi-Fi; zh-CN) com.bundle.id/3.2
-
- User-Agent: Android/4.2 (MI-ONE Plus; MIUI-2.3.6f; unrooted; GPRS; zh-TW) com.bundle.id/2.1
-
Android 的网络类型获取可以参考文档:http://developer.android.com/reference/android/telephony/TelephonyManager.html
如果是自定义的身份验证方式,比如要求请求时带上请求头 Authentication: Token <token>,那么一般在 token 验证失败返回 401 的 WWW-Authenticate 头可以是 WWW-Authenticate: Token ,当然也可以带上任意其他自定义信息。客户端在发现自己无法识别的信息时应该略过。
如果只是打算简单实现,建议使用 TOTP(Wikipedia) 协议,可以兼容 Google Authenticator 。
关于如何在 API 中实现对两步验证的支持,可以参考 GitHub 的文档。
相关资料:
请求:
- POST /resources HTTP/1.1
-
- [{
- "id": "1 也允许由客户端直接指定 ID ,比如 UUID",
- "name": "resource1",
- "property": "a"
- }, {
- "name": "resource2",
- "property": "b"
- }, {
- "name": "resource3",
- "property": "c"
- }]
响应:
- HTTP/1.1 201 Created
- Location: /resources/1,2,3
-
- [{
- "id": "1",
- "name": "resource1",
- "property": "a"
- }, {
- "id": "2",
- "name": "resource2",
- "property": "b"
- }, {
- "id": "3",
- "name": "resource3",
- "property": "c"
- }]
- // 请求
- DELETE /resources/1,2,3 HTTP/1.1
-
- // 响应
- HTTP/1.1 204 No Content
请求:
- PATCH /resources/1,2,3 HTTP/1.1
-
- Content-Type: application/json
- {
- "property": "d"
- }
-
- // 也可以使用 JSON Patch
- Content-Type: application/json-patch+json
- {
- "op": "replace",
- "replace": "/property",
- "value": "d"
- }
请求实体可以直接写一个 JSON 进行修改,也可以发送一个 JSON Patch 进行修改。
响应:
- HTTP/1.1 204 No Content
想法受启发于 JSON API 方案,做法基本照搬,主要是把 links 相关内容放到了响应头里。
可以添加 schema 参数链接到目标数据的结构描述文档,比如 JSON Schema 、 Schema.org(link:http://schema.org/) 等。
想法目前还不成熟,不建议投入使用。
- HTTP/1.1 200 OK
- Link: <http://api.example.com/peoples/{posts.author}>; rel="url-template:author"; allow="COLLECTION,GET"; schema="...",
- <http://api.example.com/comments/{posts.comments}>; rel="url-template:comments"; allow="COLLECTION,CREATE,GET,DELETE"; schema="...",
- <http://api.example.com/todos/order>; rel="url-template:order"; allow="GET,PUT"; schema="..."
-
- [{
- "id": "1",
- "title": "Rails is Omakase",
- "author": "9",
- "comments": [ "5", "12", "17", "20" ]
- }]
在调用接口的过程中,可能出现下列几种错误情况:
- HTTP/1.1 503 Service Unavailable
- Retry-After: 3600
- Content-Length: 41
-
- {"message": "Service In the maintenance"}
- HTTP/1.1 400 Bad Request
- Content-Length: 35
-
- {"message": "Problems parsing JSON"}
- HTTP/1.1 403 Forbidden
- Content-Length: 29
-
- {"message": "Service expired"}
- HTTP/1.1 403 Forbidden
- Content-Length: 29
-
- {"message": "Account blocked"}
- HTTP/1.1 403 Forbidden
- Content-Length: 31
-
- {"message": "Permission denied"}
- HTTP/1.1 404 Not Found
- Content-Length: 32
-
- {"message": "Resource not found"}
- HTTP/1.1 428 Precondition Required
- Content-Length: 35
-
- {"message": "Header User-Agent is required"}
- HTTP/1.1 422 Unprocessable Entity
- Content-Length: 149
-
- {
- "message": "Validation Failed",
- "errors": [
- {
- "resource": "Issue",
- "field": "title",
- "code": "required"
- }
- ]
- }
所有的 error 哈希表都有 resource, field, code 字段,以便于定位错误,code 字段则用于表示错误类型:
其他参考:
请求某个资源集合时,可以通过指定 count 参数来指定每页的资源数量,通过 page 参数指定页码,或根据需求使用 last_cursor 参数指定上一页最后一个资源的标识符替代 page 参数。
如果没有传递 count 参数或者 count 参数的值为空,则使用默认值,建议在设计时设置一个最大值。
分页的相关信息可以包含在 Link Header 和 X-Pagination-Info 中( HTTP 头的语法格式可以参考 ABNF List Extension: #rule )。
如果是第一页或者是最后一页时,不返回 previous 和 next 的 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"
-
- [
- ...
- ]
相关资料:
其他参考: