- 文档主要目的是为设计接口时提供建议,使大家不必重复造 HTTP 协议已经完成的轮子
- 只是建议,不是必须遵从的要求
- 大家有什么问题想法或者建议欢迎 创建 Issue 或者 提交 Pull Request
2014 年 6 月的时候 IETF 已经正式的废弃了 RFC 2616 ,将它拆分为六个单独的协议说明,并重点对原来语义模糊的部分进行了解释:
- RFC 7230 - HTTP/1.1: Message Syntax and Routing - low-level message parsing and connection management
- RFC 7231 - HTTP/1.1: Semantics and Content - methods, status codes and headers
- RFC 7232 - HTTP/1.1: Conditional Requests - e.g., If-Modified-Since
- RFC 7233 - HTTP/1.1: Range Requests - getting partial content
- RFC 7234 - HTTP/1.1: Caching - browser and intermediary caches
- RFC 7235 - HTTP/1.1: Authentication - a framework for HTTP authentication
相关资料:
HTTP 协议的 2.0 版本还没有正式发布,但目前已经基本稳定下来了。
2.0 版本的设计目标是尽量在使用层面上保持与 1.1 版本的兼容,所以,虽然数据交换的格式发生了变化,但语义基本全部被保留下来了。
因此,作为使用者而言,我们并不需要为了支持 2.0 而大幅修改代码。
HOST 地址:
https://api.example.com
所有 URI 都需要遵循 RFC 3986 的要求。
强烈建议 API 部署 SSL 证书,这样接口传递的数据的安全性才能都得一定的保障。
接口遵循“输入宽容,输出严格”原则,输出的数据结构中空字段的值一律为 null
RFC 5646 (BCP 47) 规定的语言标签的格式如下:
language-script-region-variant-extension-privateuse
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
部分
- 这个部分由
script
: 这部分使用的是 ISO 15924 (Wikipedia) 中定义的语言代码,比如简体汉字是zh-Hans
,繁体汉字是zh-Hant
。region
: 这部分使用的是 ISO 3166-1 (Wikipedia) 中定义的地理区域代码,比如zh-Hans-CN
就是中国大陆使用的简体中文。variant
: 用来表示extlang
的定义里没有包含的方言,具体的使用方法可以参考 RFC 5646 。extension
: 用来为自己的应用做一些语言上的额外的扩展,具体的使用方法可以参考 RFC 5646 。privateuse
: 用来表示私有协议中约定的一些语言上的区别,具体的使用方法可以参考 RFC 5646 。
其中只有 language
部分是必须的,其他部分都是可选的;不过为了便于编写程序,建议设计接口时约定语言标签的结构,比如统一使用 language-script-region
的形式( zh-Hans-CN
, zh-Hant-HK
等等)。
语言标签是大小写不敏感的,但按照惯例,建议 script
部分首字母大写, region
部分全部大写,其余部分全部小写。
有一点需要注意,任何合法的标签都必须经过 IANA 的认证,已通过认证的标签可以在这个网页查到。此外,网上还有一个非官方的标签搜索引擎。
相关资料:
- ISO 639-1 Code List (Wikipedia)
- ISO 639-2 Code List (Wikipedia)
- ISO 639-3 Code List
- ISO 639-5 Code List (Wikipedia)
- ISO 639-3 Macrolanguage Mappings (Wikipedia)
- Relationship between ISO 639-3 and the other parts of ISO 639
- 网页头部的声明应该是用 lang="zh" 还是 lang="zh-cn"? - 知乎
- IETF language tag - Wikipedia
- 语种名称代码 :文中对带有方言(
extlang
)部分的标签介绍有误 - Language tags in HTML and XML
- Choosing a Language Tag
客户端请求服务器时,如果对时间有特殊要求(如某段时间每天的统计信息),则可以参考 IETF 相关草案 增加请求头 Timezone
。
Timezone: 2007-06-12T23:48:22+0800
// OR
Timezone: 1977-07-30T12:00:11+0200;;Europe/Athens
时区的名称可以参考 tz datebase(Wikipedia) 。
如果客户端请求时没有指定相应的时区,则服务端默认使用 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:00Z
、2007-03-01T13:00:00Z/P1Y2M10DT2H30M
、P1Y2M10DT2H30M/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
,OPTION
,HEAD
之一,则视作相应的请求方式进行处理 GET
,DELETE
,HEAD
方法,参数风格为标准的GET
风格的参数,如url?a=1&b=2
POST
,PUT
,PATCH
,OPTION
方法- 默认情况下请求实体会被视作标准 json 字符串进行处理,当然,依旧推荐设置头信息的
Content-Type
为application/json
- 在一些特殊接口中(会在文档中说明),可能允许
Content-Type
为application/x-www-form-urlencoded
或者multipart/form-data
,此时请求实体会被视作标准POST
风格的参数进行处理
- 默认情况下请求实体会被视作标准 json 字符串进行处理,当然,依旧推荐设置头信息的
关于方法语义的说明:
OPTIONS
用于获取资源支持的所有 HTTP 方法HEAD
用于只获取请求某个资源返回的头信息GET
用于从服务器获取某个资源的信息- 完成请求后返回状态码
200 OK
- 完成请求后需要返回被请求的资源详细信息
- 完成请求后返回状态码
POST
用于创建新资源- 创建完成后返回状态码
201 Created
- 完成请求后需要返回被创建的资源详细信息
- 创建完成后返回状态码
PUT
用于完整的替换资源或者创建指定身份的资源,比如创建 id 为 123 的某个资源- 如果是创建了资源,则返回
201 Created
- 如果是替换了资源,则返回
200 OK
- 完成请求后需要返回被修改的资源详细信息
- 如果是创建了资源,则返回
PATCH
用于局部更新资源- 完成请求后返回状态码
200 OK
- 完成请求后需要返回被修改的资源详细信息
- 完成请求后返回状态码
DELETE
用于删除某个资源- 完成请求后返回状态码
204 No Content
- 完成请求后返回状态码
相关资料:
- RFC 7231 中对请求方法的定义
- RFC 5789 - PATCH 方法的定义
- 维基百科
- 200 OK : 请求执行成功并返回相应数据,如
GET
成功 - 201 Created : 对象创建成功并返回相应资源数据,如
POST
成功;创建完成后响应头中应该携带头标Location
,指向新建资源的地址 - 202 Accepted : 接受请求,但无法立即完成创建行为,比如其中涉及到一个需要花费若干小时才能完成的任务。返回的实体中应该包含当前状态的信息,以及指向处理状态监视器或状态预测的指针,以便客户端能够获取最新状态。
- 204 No Content : 请求执行成功,不返回相应资源数据,如
PATCH
,DELETE
成功
重定向的新地址都需要在响应头 Location
中返回
- 301 Moved Permanently : 被请求的资源已永久移动到新位置
- 302 Found : 请求的资源现在临时从不同的 URI 响应请求
- 303 See Other : 对应当前请求的响应可以在另一个 URI 上被找到,客户端应该使用
GET
方法进行请求 - 307 Temporary Redirect : 对应当前请求的响应可以在另一个 URI 上被找到,客户端应该保持原有的请求方法进行请求
- 304 Not Modified : 资源自从上次请求后没有再次发生变化,主要使用场景在于实现数据缓存
- 409 Conflict : 请求操作和资源的当前状态存在冲突。主要使用场景在于实现并发控制
- 412 Precondition Failed : 服务器在验证在请求的头字段中给出先决条件时,没能满足其中的一个或多个。主要使用场景在于实现并发控制
- 400 Bad Request : 请求体包含语法错误
- 401 Unauthorized : 需要验证用户身份,如果服务器就算是身份验证后也不允许客户访问资源,应该响应
403 Forbidden
- 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 响应的方式处理它。
501
与 405
的区别是:405
是表示服务端不允许客户端这么做,501
是表示客户端或许可以这么做,但服务端还没有实现这个功能
相关资料:
- RFC 里的状态码列表
- RFC 4918 - 422 状态码的定义
- RFC 6585 - 新增的四个 HTTP 状态码,中文版
- 维基百科上的《 HTTP 状态码》词条
- Do I need to use http redirect code 302 or 307? - Stack Overflow
- 400 vs 422 response to POST of data
在调用接口的过程中,可能出现下列几种错误情况:
-
服务器维护中,
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": "missing_field" } ] }
所有的 error
哈希表都有 resource
, field
, code
字段,以便于定位错误,code
字段则用于表示错误类型:
missing
: 说明某个字段的值代表的资源不存在invalid
: 某个字段的值非法,接口文档中会提供相应的信息missing_field
: 缺失某个必须的字段already_exist
: 发送的资源中的某个字段的值和服务器中已有的某个资源冲突,常见于某些值全局唯一的字段,比如 @ 用的用户名(这个错误我有纠结,因为其实有 409 状态码可以表示,但是在修改某个资源时,很一般显然请求中不止是一种错误,如果是 409 的话,多种错误的场景就不合适了)
部分接口需要通过某种身份验证方式才能请求成功(这些接口应该在文档中标注出来),合适的身份验证解决方案目前有两种:
- HTTP 基本认证,只有在部署了 SSL 证书的情况下才可以使用,否则用户密码会有暴露的风险,当然,最好不要使用
- JSON Web Token ,支持通过登录接口使用账号密码获取,在请求接口时使用
Authorization: Bearer #{token}
头标或者token
参数的值的方式进行验证。 - OAuth 2.0
REST 服务的要求之一,客户端不再需要将某些接口的 URI 硬编码在代码中,唯一需要存储的只是 API 的 HOST 地址,能够非常有效的降低客户端与服务端之间的耦合,服务端对 URI 的任何改动都不会影响到客户端的稳定。
目前只有几种方案差强人意:
- JSON HAL 草案 ,示例可以参考 JSON HAL 作者自己的介绍
- GitHub API 使用的方案 ,应该是一种 JSON HAL 的变体
- JSON API 方案 (这里有 @迷渡 发起的 中文版 ),另外一种类似 JSON HAL 的方案,不过某些方面(比如甚至也考虑到了 URL )考虑的比 JSON HAL 更为具体
目前来看应该是合并 JSON API 和 JSON HAL 两个方案的做法,各取所长,能够得到一个相对理想的方案
请求某个资源集合时,可以通过指定 count
参数来指定每页的资源数量,通过 page
参数指定页码,或根据 last_cursor
参数指定上一页最后一个资源的标识符。
如果没有传递 count
参数或者 count
参数的值为空,则使用默认值 20 , count
参数的最大上限为 100 。
如何同时传递了 last_cursor
和 page
参数,则使用 page
。
分页的相关信息会包含在 Link Header 和 X-Total-Count
中。
如果是第一页或者是最后一页时,不会返回 previous
和 next
的 Link 。
HTTP/1.1 200 OK
X-Total-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",
[
...
]
相关资料:
大部分接口应该在响应头中携带 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
相关资料:
- RFC 7232
- HTTP 缓存 - Google Developers
- RFC 2616 中缓存过期时间的算法, MDN 版, 中文版
- HTTP 协议中 Vary 的一些研究
- Cache Control 與 ETag
不严谨的实现,或者缺少并发控制的 PUT
和 PATCH
请求可能导致 “更新丢失”。这个时候可以使用 Last-Modified
和/或 ETag
头来实现条件请求,支持乐观并发控制。
下文只考虑使用 PUT
和 PATCH
方法更新资源的情况。
- 客户端发起的请求如果没有包含
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
相关资料:
- 《RESTful Web Services Cookbook 中文版》 10.4 节 《如何在服务器端实现条件 PUT 请求》
- RFC 7232 "Conditional Requests"
- Location vs. Content-Location
接口支持“跨域资源共享”(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
如果在任何 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
})
这里还有一些其他参考资料:
- 推荐参考文档 HTTP API Design Guide 来设计 REST 风格的 API ,只有以下两点我个人并不建议参考:
- Use consistent path formats
还是不建议将动作写在 URL 中,像文档中的情况,可以将这个行为抽象成一个事务资源
POST /runs/:run_id/stop-logs
或者POST /runs/:run_id/stoppers
来解决 - Paginate with Ranges
确实是一个巧妙的设计,但似乎并不符合
Content-Range
的设计意图,而且有可能和需要使用到Content-Range
的正常场景冲突(虽然几乎不可能),所以不推荐
- Use consistent path formats
还是不建议将动作写在 URL 中,像文档中的情况,可以将这个行为抽象成一个事务资源
- Best Practices for Designing a Pragmatic RESTful API
- Thoughts on RESTful API Design
- The RESTful CookBook