微软的REST API指南算是网上比较详细的关于 Restfull API 的指导方案,由于本身是英文,网上存在部分翻译,但是大部分只翻译了一小部分内容,没有全部翻译。所以抽时间做了简单的翻译和整理。翻译质量有限,望海涵。
介绍
开发者基本都通过 HTTP 接口访问微软云平台的资源。尽管每个服务通过特定语言的框架封装了它们的 API,但它们的所有操作最终都归结为 HTTP 请求。微软必须支持多种类型的客户端和服务,且不能依赖于各个开发环境丰富的框架。因此,这些准则的一个目标是确保任何支持基本 HTTP 协议的客户端都可以简单且一致地使用 Microsoft REST API。
为了给开发人员提供最流畅的开发体验,让这些 API 遵循一致的设计准则非常重要,从而使它们用起来简单直观。本文档建立了 Microsoft REST API 开发人员应该遵循的指南,以便统一一致地开发 API。
一致性的好处也会累积起来:一致性使团队拥有通用的代码、模式设计、文档风格和设计决策。
这些准则旨在实现以下目标:
- 为 Microsoft 技术平台中的所有 API 端点定义一致的实现和模式。
- 尽可能地遵循行业普遍接受的 REST/HTTP 最佳实践。
- 使所有程序开发人员都可以简单地通过 REST 接口访问 Microsoft 服务。
- 允许 Service 开发人员利用其他 Service 的工作基础来开发一致的 REST 端点。
- 允许合作伙伴(如非微软团队)使用这些准则进行其自己的 REST 端点设计。
注:本指南旨在构建符合 REST 架构风格的服务,但不涉及或要求构建遵循 REST 约束的服务。本文档中使用的“REST”术语代指具有 REST 灵魂的服务,而不是仅仅遵循 REST。
推荐阅读
了解 REST 架构风格背后的一些理念,更有助于开发优秀的基于 HTTP 的服务。如果您对 RESTful 设计不熟悉,请参阅以下优秀资源:
- REST on Wikipedia–概述 REST 背后的常见定义和核心思想。
- REST Dissertation–Roy Fielding 在网络架构中关于 REST 章节的论文,”Architectural Styles and the Design of Network-based Software Architectures”
- RFC 7231–定义 HTTP/1.1 语义的规范,被认为是权威的资源。
- REST in Practice–关于 REST 基础的书。
详解准则
准则的应用
这些准则适用于 Microsoft 或其合作伙伴公开发布的服务的所有 REST API。私人或内部的 API 也大致遵循这些准则,因为内部服务往往最终会公开。保证一致性不仅对外部客户有利,而且对内部服务的使用者也很有价值,这些准则为所有的服务提供了最佳实践。
当然有许多合理理由可以不遵循这些准则。如:实现或必须与某些外部定义的 REST API 互操作的 REST 服务必须与哪些外部的 API 兼容,而无法遵循这些准则。而还有一些服务也可能具有需要特殊性能需求,必须采用其他格式,例如二进制协议。
现有服务和服务版本控制的准则
我们不建议为了遵循这些准则,而过早的对老服务进行重大更改。无论如何,当兼容性被破坏时,该服务应该尝试在下一版本中变得合规。当一个服务添加新的 API 时,该 API 应该与同一版本的其他 API 保持一致。因此,如果服务是针对 1.0 版本的指南编写的,那么增量添加到服务的新 API 也应该遵循 1.0 版本指南。然后该服务在下一次主要版本更新时,再去遵循最新版指南。
用词释义
本文档中的”MUST”(必须),“MUST NOT”(禁止),“REQUIRED”(需要),“SHALL”(将要),“SHALL NOT”(最好不要),“SHOULD”(应该),“SHOULD NOT”(不应该),“RECOMMENDED”(推荐),“MAY”(可能),和“OPTIONAL”(可选)等关键字的详细解释见RFC 2119。
许可
本项工作的许可采用Creative Commons Attribution 4.0 International License。
分类
Microsoft REST API 准则基本要求的一方面就是服务的分类必须符合以下定义。
错误
错误或更具体的服务错误,被定义为客户端将无效数据传递给服务并且服务明确地拒绝该数据。例如无效的证书,错误的参数,未知的版本 ID 等。通常是由于客户端传递错误或无效数据的导致的服务器返回“4xx”的 HTTP 错误代码。错误不会影响整体 API 的可用性。
故障
故障(缺陷)或更具体地说服务故障被定义为服务无法正确返回数据以响应有效的客户端请求。通常会返回“5xx”HTTP 错误代码。故障会影响整体 API 的可用性。由于速率限制或配额不足导致失败的调用绝不能算作故障。由于服务快速失败(fast-failing)请求而失败的调用(通常是为了保护自己)会被视为故障。
译者注:故障意味着服务端代码出现故障,可能会影响整体的 API 使用。比如数据库连接超时。
- fast-failing 快速失败
- safe-failing 安全失败
延迟
延迟定义为具体 API 被调用完成所需的时长,尽可能用客户端进行测量。这种测量方法同样适用于同步和异步 API。对于长时间运行的调用,延迟定义为第一次调用它所需的时长,而非它长时间运行的时长。
完成时间
暴露长时间操作的服务必须跟踪这些操作的”完成时间(Time to Complete)”指标。
长时间运行故障
对于长时间运行的 API,很可能出现初始请求成功,且后续每次去获取结果时 API 也处于正常运行(每次都回传 200)中,但其底层操作已经失败了的情况。长时间运行故障必须作为故障汇总到总体可用性指标中。
客户端指南
为了保证与 REST Service 进行通讯的客户端有最好的体验,客户端应该遵循以下最佳实践:
忽略规则
对于在调用前数据格式不确定的松耦合客户端,如果服务器返回的数据格式错误,客户端必须安全地忽略掉它。
有的 Service 可能会未更新版本号就在响应中增加字段。Service 这样做的必须在它的文档中进行明确说明,且客户端必须忽略掉这些未知的字段。
译者注:一个已发布的在线接口服务,如果不修改版本而增加字段,那么一定不能影响已有的客户端调用。
变量排序规则
客户端处理响应数据时一定不能依赖服务端 JSON 响应数据字段的顺序。例如,当服务器返回的 JSON 对象中的字段顺序发生变化,客户端应当能够正确进行解析处理。
当服务端支持时,客户端可以请求以特定的顺序返回数据。例如,服务端可能支持使用 $orderBy querystring 参数来指定 JSON 数组中元素的顺序。
服务端也可以在协议中显式说明指定某些元素按特定方式进行排序。例如,服务端可以每次返回 JSON 对象时都把 JSON 对象的类型信息作为第一个字段返回,进而简化客户端解析返回数据格式的难度。客户端处理数据时可以依赖于服务端明确指定了的排序行为。
静默失败规则
当客户端请求带可选功能参数的服务时(例如带可选的头部信息),必须对服务端的返回格式有一定兼容性,可以忽略某些特定功能。
译者注:例如分页数、排序等自定义参数的支持和返回格式的兼容。
基础原则
URL 链接格式
URL 必须保证友好的可读性与可构造性,人类应该能够轻松地读取和构造 URL。这有助于用户发现并简化接口的调用,即使平台没有良好的客户端 SDK 支持。
译者注:API URL 路径结构应该是友好的易于理解的。甚至用户无需通过阅读 API 文档能够猜出相关结构和路径。
格式良好的 URL:
https://api.contoso.com/v1.0/people/jdoe@contoso.com/inbox
格式不友好的 URL:
https://api.contoso.com/EWS/OData/Users('jdoe@microsoft.com')/Folders('AAMkADdiYzI1MjUzLTk4MjQtNDQ1Yy05YjJkLWNlMzMzYmIzNTY0MwAuAAAAAACzMsPHYH6HQoSwfdpDx-2bAQCXhUk6PC1dS7AERFluCgBfAAABo58UAAA=')
出现的常见模式是使用 URL 作为值(参数)。服务可以使用 URL 作为值。
例如,以下内容是可以接受的(URL 中,url 参数传递 fancyshoes 这个资源):
https://api.contoso.com/v1.0/items?url=https://resources.contoso.com/shoes/fancy
URL 长度
HTTP 1.1 消息格式(在第 3.1.1 节的RFC 7230中定义)对请求没有长度限制,其中包括目标 URL。RFC 的:
HTTP 没有对请求行长度设置预定义的限制。[…如果服务器接收到的请求目标比它希望解析的任何 URI 都长,那么它必须使用 414(URI 太长)状态代码进行响应。
服务如果能够生成超过 2,083 个字符的 url,必须考虑兼容它支持的客户端。不同客户端支持的最长 URL 长度参见以下资料:
还请注意,一些技术栈有强制的的 URL 限定,所以在设计服务时要记住这一点。
规范的标识符
除了友好的 URL 之外,可以移动或重命名的资源也应该暴露一个包含唯一固定标识符的 URL。
在与服务进行交互时可能需要通过友好的名称来获取资源固定的 URL,例如某些服务使用的“/my”快捷方式。
指南不强制要求固定标识符使用 GUID。包含规范标识符的 URL 的一个例子是(标识符比较友好):
https://api.contoso.com/v1.0/people/7011042402/inbox
支持的方法
客户端必须尽可能使用正确的 HTTP 动词来执行操作,并且必须考虑是否支持此操作的幂等性。HTTP 方法通常称为 HTTP 动词。
这些术语在这种情况下是同义词,但 HTTP 规范了使用术语的方法。
下面是 Microsoft REST 服务应该支持的方法列表。并非所有资源都支持所有方法,但使用下面方法的所有资源必须遵从下面的用法。
方法 | 描述 | 是否幂等 |
GET | 返回对象的当前值 | True |
PUT | 替换对象,或在适用时创建一个命名对象 | True |
DELETE | 删除对象 | True |
POST | 根据提供的数据创建一个新对象,或提交一个命令 | False |
HEAD | 为 GET 响应返回对象的元数据。支持 GET 方法的资源也可以支持 HEAD 方法 | True |
PATCH | 对对象应用部分更新 | False |
OPTIONS | 获取有关 Request 的信息;详见下文。 | True |
POST
POST 操作应该支持重定向响应标头(Location),以便通过重定向标头返回创建好的资源的链接。
例如,假设一个服务允许创建并命名托管服务器:
POST http://api.contoso.com/account1/servers
响应将会是这个样子:
201 Created Location: http://api.contoso.com/account1/servers/server321
其中“server321”是服务分配的服务器名。
服务还可以在响应中返回已创建项的完整元数据。
PATCH
PATCH 已经被 IETF 标准化为递进式更新现有对象的方法(参考RFC 5789)。符合 Microsoft REST API 指南的 API 应该支持 PATCH 方法。
通过 PATCH 创建资源(UPSERT 语义)
允许客户端在创建资源的时候只指定部分键值(key)数据的必须支持 UPSET 语义,该服务必须支持以 PATCH 动词来创建资源。
鉴于 PUT 被定义为内容的完全替换,所以客户端使用 PUT 修改数据是危险的。当试图更新资源时,不理解(并因此忽略)资源的某些属性的客户端,很可能在 PUT 上忽视这些属性,导致提交后这些属性可能在不经意间被删除。所以,如果选择支持 PUT 来更新现有资源,则必须是完整替换(即,PUT 之后,资源的属性必须匹配请求中提供的内容,包括删除没有提供的任何服务端的属性)。
在 UPSERT 语义下,对不存在资源的 PATCH 调用,由服务器作为“创建”处理,对已存在的资源的 PATCH 调用作为“更新”处理。
为了确保更新请求不被视为创建(反之亦然),客户端可以在请求中指定预先定义的 HTTP 请求头。
- 如果 PATCH 请求包含 if-match 标头,则服务不能将其视为插入;如果 PATCH 请求包含值为“*”的 if-none-match 头,则服务不能将其视为更新。
如果服务不支持 UPSERT,则针对不存在的资源的 PATCH 调用必须导致 HTTP“409 Conflict”错误。
Options 标头和 link headers 标签
OPTIONS 允许客户端查询某个资源的元信息,并至少可以通过返回支持该资源的有效方法(支持的动词类别)的 Allow 标头。
此外,建议服务返回应该包括一个指向有关资源的稳定链接(Link header)(参考RFC 5988):
Link: <{help}>; rel="help"
其中 {help} 是文档资源的 URL。
有关使用 OPTIONS 的示例,请参考preflighting CORS cross-domain calls。
标准的请求标头
下面的请求标头表应该遵循 Microsoft REST API 指南服务使用。使用这些标题不是强制性的,但如果使用它们则必须始终一致地使用。
所有标头值都必须遵循规范中规定的标头字段所规定的语法规则。许多 HTTP 标头在RFC 7231 中定义,但是在IANA 标头注册表 中可以找到完整的已批准头列表。
Header | Type | Description |
Authorization | String | 请求的授权标头 |
Date | Date | 请求的时间戳,基于客户端的时钟,采用RFC 5322 日期和时间格式。服务器不应该对客户端时钟的准确性做任何假设。此标头可以包含在请求中,但在提供时必须采用此格式。当提供此报头时,必须使用格林尼治平均时间 (GMT) 作为时区参考。例如:Wed, 24 Aug 2016 18:41:30 GMT. 请注意,GMT 正好等于 UTC(协调世界时)。 |
Accept | Content type | 响应请求的内容类型,如:
l application/xml l text/xml l application/json l text/javascript (for JSONP) 根据 HTTP 准则,这只是一个提示,响应可能有不同的内容类型,例如 blob fetch,其中成功的响应将只是 blob 流作为有效负载。对于遵循 OData 的服务,应该遵循 OData 中指定的首选项顺序。 |
Accept-Encoding | Gzip, deflate | 如果适用,REST 端点应该支持 GZIP 和 DEFLATE 编码。对于非常大的资源,服务可能会忽略并返回未压缩的数据。 |
Accept-Language | “en”, “es”, etc. | 指定响应的首选语言。不需要服务来支持这一点,但是如果一个服务支持本地化,那么它必须通过 Accept-Language 头来支持本地化。 |
Accept-Charset | Charset type like “UTF-8” | 默认值是 UTF-8,但服务应该能够处理 ISO-8859-1 |
Content-Type | Content type | Mime type of request body (PUT/POST/PATCH) |
Prefer | return=minimal, return=representation | 如果指定了 return=minimal 首选项,则服务应该返回一个空主体 (empty body) 以响应一次成功插入或更新。如果指定了 return=representation,则服务应该在响应中返回创建或更新的资源。如果服务的场景中客户端有时会从响应中获益,但有时响应会对带宽造成太大的影响,那么它们应该支持这个报头。 |
If-Match, If-None-Match, If-Range | String | 使用乐观并发控制支持资源更新的服务必须支持 If-Match 标头。服务也可以使用其他与 ETag 相关的头,只要它们遵循 HTTP 规范。 |
标准响应标头
服务应该返回以下响应标头,除非在 “required” 列中注明。
Response Header | Required | Description |
Date | All responses | 根据服务器的时钟,以 RFC 5322 日期和时间格式处理响应。这个头必须包含在响应中。此报头必须使用格林尼治平均时间 (GMT) 作为时区参考。例如: Wed, 24 Aug 2016 18:41:30 GMT. 请注意,GMT 正好等于协调世界时 (UTC)。 |
Content-Type | All responses | 内容类型 |
Content-Encoding | All responses | GZIP 或 DEFLATE,视情况而定 |
Preference-Applied | 在请求中指定时 | 是否应用了首选项请求头中指示的首选项 |
ETag | 当请求的资源具有实体标记时 | ETag 响应头字段为请求的变量提供实体标记的当前值。与 If-Match、If-None-Match 和 If-Range 一起使用,实现乐观并发控制。 |
自定义标头
基本的 API 操作不应该支持自定义标头。
本文档中的一些准则规定了非标准 HTTP 标头的使用。此外,某些服务可能需要添加额外的功能,这些功能通过 HTTP 标头文件公开。以下准则有助于在使用自定义标头时保持一致性。
非标准 HTTP 标头必须具有以下两种格式之一:
- 使用 IANA(RFC 3864)注册为 “临时” 的标头的通用格式
- 为注册使用过特定的头文件的范围格式
这两种格式如下所述。
以查询参数方式提交自定义请求头
有些标头对某些场景 (如 AJAX 客户端) 不兼容,特别是在不支持添加标头的跨域调用时。因此,除了常见的标头信息外,一些标头信息可以允许被作为查询参数传递给服务端,其命名与请求头中的名称保持一致。
并不是所有的标头都可以用作查询参数,包括大多数标准 HTTP 标头。考虑何时接受标头作为参数的标准如下:
- 任何自定义标头也必须作为参数接受。
- 请求的标准标头也可以作为参数接受。
- 具有安全敏感性的必需标头 (例如,授权标头 Authorization) 可能不适合作为参数; 服务所有者应该具体情况具体分析。
此规则的一个例外是 Accept 头。使用具有简单名称的方案,而不是使用 HTTP 规范中描述的用于 Accept 的完整功能,这是一种常见的实践。
PII 个人身份信息参数
与普遍的隐私政策一致,客户端不应该在 URL 中传输个人身份信息 (PII) 参数 (作为路径或查询参数),因为这些信息可能通过客户端、网络和服务器日志和其他机制无意暴露出来。因此,服务应该接受 PII 参数作为标头传输。
然而在实践中,由于客户端或软件的限制,在许多情况下无法遵循上述建议。为了解决这些限制,服务也应该接受这些 PII 参数作为 URL 的一部分,与本指导原则的其余部分保持一致。
接受 PII 参数 (无论是在 URL 中还是作为标头) 的服务应该符合其组织的隐私保护原则。通常建议包括:客户端使用标头进行加密传输,并且实现要遵循特殊的预防措施,以确保日志和其他服务数据收集得到正确的处理。
响应格式
一个成功的平台,往往以开发人员习惯使用的格式提供数据,并以一致的方式提供数据,并允许开发人员能够使用通用代码处理响应。
基于Web的通信,特别是当涉及移动或其他低带宽客户端时,由于各种原因,已经朝着JSON的方向快速发展,包括其重量更轻的趋势以及易于使用基于JavaScript的客户端。
基于Web的通信,特别是当涉及移动端或其他低带宽客户端时,我们推荐使用JSON作为传输格式。主要是由于其更轻量以及易于与JavaScript交互。
JSON属性名应该采用camelCased驼峰式命名规范。
服务应该提供JSON作为默认输出格式。
客户端指定响应格式
在HTTP中,客户端应该使用Accept头请求响应格式。服务端可以选择性的忽略,如客户端发送多个Accept标头,服务可以选择其中一个格式进行响应。
默认的响应格式(没有提供Accept头)应该是application/json,并且所有服务必须支持application/json。
AcceptHeader | Responsetype | Notes |
application/json | 必须是返回json格式 | 同样接受JSONP请求的text/JavaScript |
GET https://api.contoso.com/v1.0/products/user Accept: application/json
错误的条件响应
对于调用不成功的情况,开发人员应该能够用相同的代码库一致地处理错误。这允许构建简单可靠的基础架构来处理异常,将异常作为成功响应的独立处理流程来处理。下面的代码基于ODatav4JSON规范。但是,它非常通用,不需要特定的OData构造。即使api没有使用其他OData结构,也应该使用这种格式。
- 错误响应必须是单个JSON对象。该对象必须有一个名为“error”的键/值(name/value)对。该值必须是JSON对象。
- 这个对象必须包含名称“code”和“message”的键值对,并且它建议包含譬如“target”、“details”和“innererror”的键值对。
- “code”键值对的值是一个与语言无关的字符串。它的值是该服端务定义的错误代码,应该简单可读。与响应中指定的HTTP错误代码相比,此代码用作错误的更具体的指示。服务应该具有相对较少的“code”数量(别超过20个),并且所有客户端必须能够处理所有这些错误信息。大多数服务将需要更大数量的更具体的错误代码以满足所有的客户端请求。这些错误代码应该在“innererror”键值对中公开,如下所述。为现有客户端可见的“代码”引入新值是一个破坏性的更改,需要增加版本。服务可以通过向“innererror”添加新的错误代码来避免中断服务更改。
- “message”键值对的值必须是错误提示消息,必须是可读且易于理解。它旨在是帮助开发人员,不适合暴露给最终用户。想要为最终用户公开合适消息的服务必须通过annotation注释或其他自定义属性来公开。服务不应该为最终用户本地化“message”,因为这样对于开发者变得非常不友好并且难以处理。
- “target”键值对的值是指向错误的具体的目标(例如,错误中属性的名称)。
- “details”键值对的值必须是JSON对象数组,其中必须包含“code”和“message”的键值对,还可能包含“target”的键值对,如上所述。“details”数组中的对象通常表示请求期间发生的不同的、相关的错误。请参见下面的例子。
- “innererror”键值对的值必须是一个对象。这个对象的内容是服务端定义的。想要返回比根级别代码更具体的错误的服务,必须包含“code”的键值对和嵌套的“innererror”。每个嵌套的“innererror”对象表示比其父对象更高层次的细节。在评估错误时,客户端必须遍历所有嵌套的“内部错误”,并选择他们能够理解的最深的一个。这个方案允许服务在层次结构的任何地方引入新的错误代码,而不破坏向后兼容性,只要旧的错误代码仍然出现。服务可以向不同的调用者返回不同级别的深度和细节。例如,在开发环境中,最深的“innererror”可能包含有助于调试服务的内部信息。为了防范信息公开带来的潜在安全问题,服务应注意不要无意中暴露过多的细节。错误对象还可以包括特定于代码的自定义服务器定义的键值对。带有自定义服务器定义属性的错误类型应该在服务的元数据文档中声明。请参见下面的例子。
错误响应返回的的任何JSON对象中都可能包含注释。
我们建议,对于任何可能重试的临时错误,服务应该包含一个Retry-After HTTP头,告诉客户端在再次尝试操作之前应该等待的最小秒数。
ErrorResponse: Object
Property | Type | Required | Description |
error | Error | Yes | The error object. |
Error: Object
Property | Type | Required | Description |
code | String(enumerated) | Yes | 服务器定义的错误代码集之一 |
message | String | Yes | 一个可读性较高的错误信息 |
target | String | 指向错误的具体的目标 | |
details | Error[] | 有关导致此报告错误的特定错误的详细信息数组。 | |
innererror | InnerError | 包含比当前对象更具体的错误信息的对象。 |
InnerError: Object
Property | Type | Required | Description |
code | String | 一个比包含错误的程序提供的更具体的错误代码。 | |
innererror | InnerError | 包含比当前对象更具体的错误信息的对象。 |
Examples
内部错误的例子:
{ "error": { "code": "BadArgument", "message": "Previous passwords may not be reused", "target": "password", "innererror": { "code": "PasswordError", "innererror": { "code": "PasswordDoesNotMeetPolicy", "minLength": "6", "maxLength": "64", "characterTypes": ["lowerCase", "upperCase", "number", "symbol"], "minDistinctCharacterTypes": "2", "innererror": { "code": "PasswordReuseNotAllowed" } } } } }
在本例中,基本的错误代码是”BadArgument”,但是对于感兴趣的客户端,”innererror”中提供了更具体的错误代码。”password reuse not allowed”代码可能是在之后的迭代中由该服务添加的,之前只返回”password not meet policy”。这种增量型的添加方式并不会破坏老的客户端的处理过程,而又可以给开发者一些更详细的信息。”PasswordDoesNotMeetPolicy”错误还包括额外的键值对,这些键值对允许客户机确定服务器的配置、以编程方式验证用户的输入,或者在客户机自己的本地化消息传递中向用户显示服务器的约束。
详细的例子”details”:
{ "error": { "code": "BadArgument", "message": "Multiple errors in ContactInfo data", "target": "ContactInfo", "details": [ { "code": "NullValue", "target": "PhoneNumber", "message": "Phone number must not be null" }, { "code": "NullValue", "target": "LastName", "message": "Last name must not be null" }, { "code": "MalformedValue", "target": "Address", "message": "Address is not valid" } ] } }
在本例中,请求存在多处问题,每个错误都列在”details”字段中进行返回了。
HTTP状态码
应使用标准HTTP状态码作为响应状态码;更多信息,请参见HTTP状态代码定义。
客户端库可选
开发人员必须能够在各种平台和语言上进行开发,比如Windows、macOS、Linux、C#、Python和Node.js或是Ruby。
服务应该能够让简单的HTTP工具进行访问,而不需要做太多的工作。
该服务提供给开发人员的网站应该提供相当于”获得开发者令牌(Get developer Token)”的功能,以帮助开发人员测试并应提供curl支持。
CORS跨域
符合Microsoft REST API准则的服务必须支持CORS(跨源资源共享)。服务应该支持CORS*的允许起源,并通过有效的OAuth令牌强制授权。服务不应该支持带有源验证的用户凭据。特殊情况可例外。
客户端指导
Web开发人员通常不需要做任何特殊处理来利用CORS。作为标准XMLHttpRequest调用的一部分,所有握手步骤都是不可见的。
许多其他平台(如.NET)已集成了对CORS的支持。
避免额外的预检查
由于CORS协议会触发向服务器添加额外往返的预检请求,因此,注重性能的应用程序可能会有意避免这些请求。CORS背后的精神是避免对旧的不支持CORS功能的浏览器能够做出的任何简单的跨域请求进行预检。所有其他请求都需要预检。
请求是”简单类型请求”,如果其方法是GET,HEAD或POST,并且除了Accept,Accept-Language和Content-Language之外它不包含任何请求标头,则可以免去预检。
对于POST请求,也允许使用Content-Type标头,但前提是其值为”application/x-www-form-urlencoded”,”multipart/form-data”或”text/plain”。对于任何其他标头或值,将发生预检请求。
服务指南
服务必须至少:
- 了解浏览器在跨域请求上发送的Origin请求标头,以及他们在检查访问权限的预检OPTIONS请求上发送的Access-Control-Request-Method请求标头。
- 如果请求中存在Origin标头:
- 如果请求使用OPTIONS方法并包含Access-Control-Request-Method标头,则它是一个预检请求,旨在在实际请求之前探测访问。否则,它是一个实际请求。对于预检请求,除了执行以下步骤添加标头之外,服务必须不执行其他处理并且必须返回200 OK。对于非预检请求,除了请求的常规处理之外,还添加了以下标头。
- 向响应添加一个Access-Control-Allow-Origin标头,包含与Origin请求标头相同的值。请注意,这需要服务动态生成标头值。不需要cookie或任何其他形式的用户凭据的资源可以用通配符星号(*)代替。请注意,通配符仅在此处可接受,不适用于下面描述的任何其他标头。
- 如果调用者需要访问不在简单响应头集合(Cache-Control、Content-Language、Content-Type、Expires、Last-Modified、Pragma)中的响应头,则添加一个Access-Control-Expose-Headers标头包含客户端应有权访问的附加响应标头名称列表。
- 如果请求需要cookie,则添加设置为”true”的Access-Control-Allow-Credentials标头。
- 如果请求是预检请求(参见第一个项目符号),则服务必须:
- 添加一个Access-Control-Allow-Headers响应标头,其中包含允许客户端使用的请求标头名称列表。此列表只需要包含不在简单请求头(Accept、Accept-Language、Content-Language)集合中的标头。如果服务接受的头部没有限制,服务可以简单地返回与客户端发送的Access-Control-Request-Headers头部相同的值。
- 添加一个Access-Control-Allow-Methods响应头,其中包含允许调用者使用的HTTP方法列表。
添加一个Access-Control-Max-Age响应头,其中包含此预检响应有效的秒数(因此可以在后续实际请求之前避免)。注意,虽然习惯上使用较大的值,比如2592000(30天),但是许多浏览器会自动设置一个更低的限制(例如,5分钟)。
因为浏览器预检响应缓存是出了名的弱,预检响应的额外往返会损害性能。
译者注:获取预检OPTIONS调用会造成很大开销,而且浏览器的缓存能力也很弱,而且部分浏览器也不会理会access-control-max-age的设置值,如Chrome/Blink就硬编码为10分钟(600秒)。详见
注重性能端的交互式Web客户端使用的服务端应该避免使用导致预检的请求。
- 对于GET和HEAD调用,请避免要求不属于上述简单集的请求标头。最好是允许将它们作为查询参数提供。
- Authorization 标头不是简单集的一部分,因此对于需要验证的资源,必须通过“access_token”查询参数发送验证令牌。请注意,不建议在 URL 中传递身份验证令牌,因为它可能导致令牌记录在服务器日志中,并暴露给有权访问这些日志的任何人。通过 URL 接受身份验证令牌的服务必须采取措施来降低安全风险,例如使用短期身份验证令牌,禁止记录身份验证令牌以及控制对服务器日志的访问。
- 需要基于 cookie 的身份验证的服务必须使用“动态验证码(dynamic canary)”[*]译者注:服务器生成某种验证码,客户端获取后,服务器再进行验证的操作。来保护所有接受 cookie 的 API。
- 服务不得以避免 CORS 预检请求的名义违反其他 API 指南。由于内容类型的原因,大多数 POST 请求实际上需要预检请求。
- 如果非要取消预检工作,那么服务支持 的其他的替代数据传输机制必须遵循本指南。
此外,当适当的服务可以支持 JSONP 模式时,只需简单的 GET 跨域访问。在 JSONP 中,服务采用指示格式的参数($format=json)和表示回调的参数($callback=someFunc),并返回一个 text/javascript 文档,其中包含用指定名称封装在函数调用中的 JSON 响应。更多关于 JSONP 的信息,请访问JSONP。
Collections 集合
元素 keys
服务可以支持集合中每个项的持久标识符(主键),该标识符应用 JSON 表示为”id”, 这些持久标识符通常用作项目的 key。
支持持久标识符(主键)的集合可以支持增量查询。
序列化
集合使用标准数组表示法以 JSON 表示。
集合的 URL 匹配
集合在顶级时直接位于服务的根目录下,或者作用于该资源时作为另一个资源下的段。
例如:
GET https://api.contoso.com/v1.0/people
服务必须尽可能支持“/”匹配。例如:
GET https://{serviceRoot}/{collection}/{id}
其中:
- {serviceRoot} – 站点 URL(site URL) + 服务的根路径的组合
- {collection} – 集合的名称,未缩写,复数
- {id} – 唯一 id 属性的值. 当使用“/“匹配必须属于 string/number/guid value 不带引号,转义正确以适应 URL。
嵌套集合和属性
集合项可以包含其他集合。例如,用户集合可能包含多个地址的用户资源:
GET https://api.contoso.com/v1.0/people/123/addresses { "value":[ {"street":"1st Avenue","city":"Seattle"}, {"street":"124th Ave NE","city":"Redmond"} ] }
大集合
随着数据的增长,集合也在增长。所以计划采用分页对所有服务都很重要。因此,当数据包含多页时,序列化有效负载(payload)必须适当地包含下一页的不透明 URL。有关详细信息,请参阅分页指南。
客户端必须能够恰当的处理请求返回的任何给定的分页或非分页集合数据。
{ "value":[ {"id":"Item1","price":99.95,"sizes":null}, {…}, {…}, {"id":"Item99","price":59.99,"sizes":null} ], "@nextLink":"{opaqueUrl}" }
集合的更改
POST 请求不是幂等的。这意味着发送到具有完全相同的有效负载(payload)的集合资源的两次 POST 请求可能导致在该集合中创建多个项。[*]译者注:相同的数据两次 POST 操作,可能导致该集合创建多次。例如,对于具有服务器端生成的 id 的项的插入操作,通常就是这种情况。
例如,以下请求:
POST https://api.contoso.com/v1.0/people
会导致响应,指示新集合项的位置:
201 Created Location: https://api.contoso.com/v1.0/people/123
一旦再次执行,可能会导致创建另一个资源:
201 Created Location: https://api.contoso.com/v1.0/people/124
而 PUT 请求则需要使用相应的键来指示集合项:
PUT https://api.contoso.com/v1.0/people/123
集合的排序
可以基于属性值对集合查询的结果进行排序。该属性由 _$orderBy_ 查询参数的值确定。
$orderBy 参数的值包含用于对项目进行排序表达式列表,用逗号分隔的。这种表达式的特殊情况是属性路径终止于基本属性。
表达式可以包含升序的后缀“asc”或降序的后缀“desc”,它们与属性名之间用一个或多个空格分隔。如果没有指定“asc”或“desc”,则服务必须按照指定的属性以升序排序。
空值(NULL)必须排序为“小于”非空值。
必须根据第一个表达式的结果值对项进行排序,然后根据第二个表达式的结果值对第一个表达式具有相同值的项进行排序,以此类推。排序顺序是属性类型的固有顺序。
例如:
GET https://api.contoso.com/v1.0/people?$orderBy=name
将返回按 name 进行升序排序的所有人员。
GET https://api.contoso.com/v1.0/people?$orderBy=name desc
将返回按 name 进行降序排序的所有人。
可以通过逗号分隔的属性名称列表以及可选方向限定符来指定子排序。
例如:
GET https://api.contoso.com/v1.0/people?$orderBy=name desc,hireDate
将返回按姓名降序排列的所有人员,并按雇佣日期降序排列的次要排序。
排序必须与筛选相结合,如下:
GET https://api.contoso.com/v1.0/people?$filter=name eq 'david'&$orderBy=hireDate
将返回所有名称为 David 的人,按雇佣日期按升序排列。
排序表达式注解
跨页面的排序参数必须一致,因为客户端和服务器端分页都依赖该排序该参数进行排序。
如果服务不支持按 _$orderBy_ 表达式中命名的属性排序,则服务必须按照“响应不支持的请求”部分中定义的错误消息进行响应。
Filtering 过滤
$filter_querystring 参数允许客户端通过 URL 过滤集合。使用 _$filter_ 指定的表达式将为集合中的每个资源求值,只有表达式求值为 true 的项才包含在响应中。表达式计算为 false 或 null 的资源,或由于权限而不可用的引用属性,将从响应中省略。
例如: 返回所有产品的价格低于 10.00 美元
GET https://api.contoso.com/v1.0/products?$filter=pricelt10.00
$filter_ 选项的值是一个布尔表达式表示 price less than 10.00。
过滤操作符
支持 _$filter_ 的服务应该支持以下最小操作集。
Operator | Description | Example |
Comparison Operators | ||
eq | Equal | city eq ‘Redmond’ |
ne | Not equal | city ne ‘London’ |
gt | Greater than | price gt 20 |
ge | Greater than or equal | price ge 10 |
lt | Less than | price lt 20 |
le | Less than or equal | price le 100 |
Logical Operators | ||
and | Logical and | price le 200 and price gt 3.5 |
or | Logical or | price le 3.5 or price gt 200 |
not | Logical negation | not price le 3.5 |
Grouping Operators | ||
() | Precedence grouping | (priority eq 1 or city eq ‘Redmond’) and price gt 100 |
操作符示例
下面的示例说明了每个逻辑操作符的用法和语义。
示例: 所有名称等于“Milk”的产品
GET https://api.contoso.com/v1.0/products?$filter=name eq 'Milk'
示例: 所有名称不等于“Milk”的产品
GET https://api.contoso.com/v1.0/products?$filter=name ne 'Milk'
示例: 所有标有“Milk”的产品价格都低于 2.55:
GET https://api.contoso.com/v1.0/products?$filter=name eq 'Milk' and price lt 2.55
示例: 所有标有“Milk”字样或价格低于 2.55 美元的产品:
GET https://api.contoso.com/v1.0/products?$filter=name eq 'Milk' or price lt 2.55
示例: 所有名称为“牛奶”或“鸡蛋”且价格低于 2.55 的产品:
GET https://api.contoso.com/v1.0/products?$filter=(name eq 'Milk' or name eq 'Eggs') and price lt 2.55
操作符优先级
在计算 _$filter_ 表达式时,服务使用以下操作符优先级。操作符按类别按优先级从高到低排列。同一类别的运算符具有同等优先级:
Group | Operator | Description |
Grouping | () | Precedence grouping |
Unary | not | Logical Negation |
Relational | gt | Greater Than |
ge | Greater than or Equal | |
lt | Less Than | |
le | Less than or Equal | |
Equality | eq | Equal |
ne | Not Equal | |
Conditional AND | and | Logical And |
Conditional OR | or | Logical Or |
Pagination 分页
返回集合的 RESTful API 可能返回部分集。这些服务的消费者清楚将获得部分结果集,并能正确地翻页以检索整个结果集。
RESTful API 可能支持两种形式的分页。
- 服务器驱动的分页:通过在多个响应有效载荷上强制分页请求来减轻拒绝服务攻击。
- 客户端驱动的分页:允许客户机只请求它在给定时间可以使用的资源数量。
跨页面的排序和筛选参数必须一致,因为客户端和服务器端分页都完全兼容于筛选和排序。
服务器驱动的分页
分页响应必须通过在响应中包含延续分页标记来告诉客户端这是部分结果。没有延续分页标记意味着没有下一页了。
客户端必须将延续 URL 视为不透明的,这意味着在迭代一组部分结果时,查询选项可能不会更改。
例如:
GET http://api.contoso.com/v1.0/people HTTP/1.1 Accept: application/json HTTP/1.1 200 OK Content-Type: application/json { ..., "value": [...], "@nextLink": "{opaqueUrl}" }
客户端驱动的分页
客户端可以使用 $top 和 $skip 查询参数来指定返回的结果数量和跳过的集合数量。
服务器应遵守客户端指定的参数;但是,客户端必须做好准备处理包含不同页面大小的响应或包含延续分页标记的响应。
当客户端同时提供 $top 和 $skip 时,服务器应该首先在集合上应用 $skip 然后应用 $top。
注意:如果服务器不能接受 $top 和/或 $skip,服务器必须向客户端返回一个错误通知,而不是仅仅忽略查询选项。这将避免客户端对返回的数据做出假设的风险。
实例:
GET http://api.contoso.com/v1.0/people?$top=5&$skip=2 HTTP/1.1 Accept: application/json HTTP/1.1 200 OK Content-Type: application/json { ..., "value": [...] }
其他注意事项
- 固定的顺序先决条件:两种分页形式都依赖于具有固定顺序的项的集合。服务器必须使用额外的排序(通常是按键排序)来补充任何指定的顺序标准,以确保项目始终保持一致的顺序。
- 缺失/重复结果:即使服务器强制执行一致的排序顺序,结果也可能会因创建或删除其他资源而导致丢失或重复。客户端必须准备好处理这些差异。服务器应该总是编码最后读取记录的记录ID,帮助客户端管理重复/丢失的结果。
- 结合客户端和服务驱动的分页:请注意,客户端驱动的分页不排除服务器驱动的分页。如果客户端请求的页面大小大于服务器支持的默认页面大小,则预期响应将是客户端指定的结果数,否则按服务端分页设置的指定分页。
- 页面大小:客户端可以通过指定 $maxpagesize 首选项来请求具有特定页面大小的服务端驱动的分页。如果指定的页面大小小于服务端的默认页面大小,服务器应该遵循此首选项。
- 分页嵌入式集合:客户端驱动的分页和服务端驱动的分页都可以应用于嵌入式集合。如果服务端对嵌入式集合进行分页,则必须包含其他适当的延续分页标记。
- 记录集计数:想要知道所有页面中的完整记录数的开发人员可以包含查询参数 _$count=true_,以告知服务端包含响应中的记录数。
复合集合操作
筛选、排序和分页操作都可以针对给定的集合执行。当这些操作一起执行时,评估顺序必须是:
- 筛选。这包括作为 AND 操作执行的所有范围表达式。
- 排序。可能已过滤的列表根据排序条件进行排序。
- 分页。经过筛选和排序的列表上显示了实现分页视图。这适用于服务器驱动的分页和客户端驱动的分页。
空结果集
当对集合执行过滤器并且结果集为空时,您必须使用有效的响应正文和 200 响应代码进行响应。在这个例子中,客户端提供的过滤器导致一个空的结果集。响应正文正常返回,并且 value 属性设置为空集合。根据您对产生结果的类似调用的响应格式,客户端可能期望像 maxItems 这样的元数据属性。您应该尽可能保持 API 的一致性。
GET https://api.contoso.com/v1.0/products?$filter=(name eq 'Milk' or name eq 'Eggs') and price lt 2.55 Accept: application/json HTTP/1.1 200 OK Content-Type: application/json { ..., "maxItems": 0, "value": [] }
增量查询
服务可以选择支持 Delta 查询。
增量链接
增量(Delta)链接是不透明的、由服务生成的链接,客户端使用这些链接查询对结果的后续更改。
在概念层面上,delta 链接基于一个定义查询,该查询描述正在跟踪更改的一组结果集。delta 链接编码并跟踪这些更改的实体集合,以及跟踪更改的起点。
如果查询包含筛选器,则响应必须只包含对匹配指定条件的实体的更改。Delta 查询的主要原则是:
- 集合中的每个项目必须具有持久标识符(永久不变的主键)。该标识符应该表示为“id”。此标识符由服务定义,客户端可以使用该字符串跨调用跟踪对象。
- delta 必须包含每个与指定条件新匹配的实体的条目,并且必须为每个不再符合条件的实体包含“@removed”条目。
- 重新调用查询并将其与原始结果集进行比较;必须将当前集合中惟一的每个条目作为”add”操作返回,并且必须将原始集合中惟一的每个条目作为“remove”操作返回。。
- 以前与标准不匹配但现在匹配的每个实体必须作为”add”返回;相反,先前与查询匹配但不再必须返回的每个实体必须作为“@removed”条目返回。
- 已更改的实体必须使用其标准表示形式包含在集合中。
- 服务可以向“@remove”节点添加额外的元数据,例如删除的原因或“removedat”时间戳。我们建议团队与 Microsoft REST API 指导原则工作组协调,以帮助维护一致性。
Delta 链接不能编码任何客户端 top 或 skip 值。
实体表示
添加和更新的实体使用其标准表示在实体集中表示。从集合的角度来看,添加或更新的实体之间没有区别。
删除的实体仅使用其“id”和“@removed”节点表示。“@removed”节点的存在必须表示从集合中删除条目。
获取增量链接
通过查询集合或实体并附加 $delta 查询字符串参数来获得 Delta 链接。
例如:
GET https://api.contoso.com/v1.0/people?$delta HTTP/1.1 Accept: application/json HTTP/1.1 200 OK Content-Type: application/json { "value": [ {"id": "1", "name": "Matt"}, {"id": "2", "name": "Mark"}, {"id": "3", "name": "John"} ], "@deltaLink": "{opaqueUrl}" }
注意:如果集合分页,deltaLink 将只出现在最后一页,但必须反映对所有页面返回的数据的任何更改。
增量链接响应的内容
添加/更新的条目必须以常规 JSON 对象的形式出现,并带有常规项目属性。在常规表示中返回添加/修改的项,允许客户端使用基于“id”字段的标准合并概念将它们合并到现有的“缓存”中。
从定义的集合中删除的条目必须包含在响应中。从集合中删除的项必须仅使用它们的“id”和“@remove”节点表示。
使用增量链接
客户端通过调用 delta 链接上的 GET 方法请求更改。客户端必须按原样使用 delta URL——换句话说,客户端不能以任何方式修改 URL(例如,解析 URL 并添加额外的查询字符串参数)。
在这个例子中:
GET https://{opaqueUrl} HTTP/1.1 Accept: application/json HTTP/1.1 200 OK Content-Type: application/json { "value": [ {"id": "1", "name": "Mat"}, {"id": "2", "name": "Marc"}, {"id": "3", "@removed": {}}, {"id": "4", "name": "Luc"} ], "@deltaLink": "{opaqueUrl}" }
针对 delta 链接的请求的结果可以跨多个页面,但是必须由服务跨所有页面进行排序,以便在应用到包含 delta 链接的响应时确保得到确定的结果。
如果没有发生任何更改,则响应是一个空集合,其中包含一个 delta 链接,用于根据请求进行后续更改。这个 delta 链接可能与 delta 链接相同,从而导致更改的空集合。
如果 delta 链接不再有效,则服务必须使用 _410 Gone_ 响应。响应应该包含一个 Location 头,客户端可以使用它来检索新的基线结果集。
JSON 标准化
原始类型的 JSON 格式标准化
原始值必须按照 [RFC8259] 的规则序列化为 JSON。
64 位整数的重要说明:JavaScript 将自动截断大于 Number.MAX_SAFE_INTEGER(2^53-1) 的整数或小于 Number.MIN_SAFE_INTEGER(-2^53+1) 的数字。如果服务预期返回安全值范围之外的整数值,请强烈考虑将该值作为字符串返回,以最大限度地提高互操作性并避免数据丢失。
日期和时间指南
生产日期
服务必须使用 DateLiteral 格式生成日期,并且应该使用 Iso8601Literal 格式,除非有令人信服的理由不这样做。使用 StructuredDateLiteral 格式的服务不得使用 T 类型生成日期,除非需要额外的精度,并且明确不支持 ECMAScript 客户端。(非规范性声明:在决定标准化哪个特定 DateKind 时,偏好的大致顺序是 E、C、U、W、O、X、I、T。这针对 ECMAScript、.NET 和 C++ 程序员进行了优化。)
消费日期
服务必须接受来自使用相同 DateLiteral 格式(包括 DateKind,如果适用)的客户端的日期,并且应该接受使用任何 DateLiteral 格式的日期。
兼容性
服务必须对相同类型的所有资源使用相同的 DateLiteral 格式(包括相同的 DateKind,如果适用),并且应该对整个服务中的所有资源使用相同的 DateLiteral 格式(和 DateKind,如果适用)。
服务产生的 DateLiteral 格式(包括 DateKind,如果适用)的任何更改以及服务接受的 DateLiteral 格式(和 DateKind,如果适用)的任何减少都必须被视为破坏性更改。服务接受的 DateLiteral 格式的任何扩展不被视为重大更改。
日期和时间的 JSON 序列化
使用 JSON 来回序列化日期是一个难题。尽管 ECMAScript 支持大多数内置类型的文字,但它没有为日期定义文字格式。Web 已经围绕ISO8601 日期格式(ISO8601) 的 ECMAScript 子集 进行了合并,但在某些情况下这种格式并不理想。对于这些情况,本文档定义了一种 JSON 序列化格式,可用于明确表示不同格式的日期。其他序列化格式(例如 XML)可以从这种格式派生出来。
日期文字格式
JSON 中表示的日期使用以下语法进行序列化。非正式地,DateValue 是 ISO8601 格式的字符串或包含两个名为 kind 和 value 的属性的 JSON 对象,它们共同定义了一个时间点。以下不是上下文无关的语法;特别是,DateValue 的解释取决于 DateKind 的值,但这最大限度地减少了描述格式所需的产品数量。
DateLiteral: Iso8601Literal StructuredDateLiteral Iso8601Literal: A string literal as defined in https://www.ecma-international.org/ecma-262/5.1/#sec-15.9.1.15. Note that the full grammar for ISO8601 (such as "basic format" without separators) is not supported. All dates default to UTC unless specified otherwise. StructuredDateLiteral: {DateKindProperty, DateValueProperty} {DateValueProperty, DateKindProperty} DateKindProperty "kind": DateKind DateKind: "C"; see below "E"; see below "I"; see below "O"; see below "T"; see below "U"; see below "W"; see below "X"; see below DateValueProperty: "value": DateValue DateValue: UnsignedInteger; not defined here SignedInteger; not defined here RealNumber; not defined here Iso8601Literal; as above
关于日期格式的评论
使用 Iso8601Literal 产生式的 DateLiteral 相对简单。下面是一个对象的示例,该对象的属性名为 creationDate,该属性设置为 2015 年 2 月 13 日下午 1:15。世界标准时间:
{"creationDate": "2015-02-13T13:15Z"}
StructuredDateLiteral 由 DateKind 和随附的 DateValue 组成,其有效值(及其解释)取决于 DateKind。下表描述了有效组合及其含义:
DateKind | DateValue | ColloquialName & Interpretation | MoreInfo |
C | UnsignedInteger | “CLR”; 自 0001 年 1 月 1 日午夜起的毫秒数;不允许出现负值。见下面的注释。 | MSDN |
E | SignedInteger | “ECMAScript”; 自 1970 年 1 月 1 日午夜以来的毫秒数。 | ECMA International |
I | Iso8601Literal | “ISO8601”; 限制为 ECMAScript 子集的字符串。 | |
O | RealNumber | “OLEDate”; 整数部分是自 1899 年 12 月 31 日午夜起的天数,分数部分是一天中的时间(0.5=正午)。 | MSDN |
T | SignedInteger | “Ticks”; 1601 年 1 月 1 日午夜以来的滴答数(100 纳秒间隔)。见下面的注释。 | MSDN |
U | SignedInteger | “UNIX”; 自 1970 年 1 月 1 日午夜以来的秒数。 | MSDN |
W | SignedInteger | “Windows”; 1601年1月1日午夜以来的毫秒数。见下面的注释。 | MSDN |
X | RealNumber | “Excel”; as for O but the year 1900 is incorrectly treated as a leap year, and day 0 is “January 0 (zero).” | Microsoft Support |
C和W类型的重要说明:本机CLR和Windows时间由100纳秒“刻度”值表示。为了与精度有限的ECMAScript客户端进行互操作,当(反)序列化为DateLiteral时,这些值必须在毫秒之间进行转换。一毫秒相当于10,000个滴答声。
T类型的重要说明:这种类型保留了Windows本机时间格式的完整保真度(并且可以轻松地与本机CLR格式进行转换),但与ECMAScript客户端不兼容。因此,它的使用应该仅限于那些既需要额外精度又不需要与ECMAScript客户端互操作的场景。
以下是具有名为creationDate的属性设置为2015年2月13日下午1:15的同一对象示例。UTC,使用多种格式:
[ {"creationDate":{"kind":"O","value":42048.55}}, {"creationDate":{"kind":"E","value":1423862100000}} ]
将种类与值分开的好处之一是,一旦客户端知道特定服务使用的种类,它就可以解释该值而无需任何额外的解析。在值是数字的常见情况下,这使开发人员更容易编码:
// We know this service always gives out ECMAScript-format dates var date = new Date(serverResponse.someObject.creationDate.value);
Durations 持续时间
Durations需要按照ISO 8601进行序列化。持续时间“由格式P[n]Y[n]M[n]DT[n]H[n]M[n]S表示”。从标准:
- P是放置在持续时间表示开头的持续时间指示符(历史上称为“句点”)。
- Y是跟在年数值之后的年份指示符。
- M是跟在月数值之后的月份指示符。
- W是跟在周数值之后的周指示符。
- D是跟在天数值之后的天指示符。
- T是在表示的时间分量之前的时间指示符。
- H是跟在小时数值之后的小时指示符。
- M是跟在分钟数值之后的分钟指示符。
- S是秒数值之后的第二个指示符。
例如,“P3Y6M4DT12H30M5S”表示“3年6个月4天12小时30分5秒”的持续时间。
Intervals 间隔
- 开始和结束,例如“2007-03-01T13:00:00Z/2008-05-11T15:30:00Z”
- 开始和持续时间,例如“2007-03-01T13:00:00Z/P1Y2M10DT2H30M”
- 持续时间和结束时间,例如“P1Y2M10DT2H30M/2008-05-11T15:30:00Z”
- 仅持续时间,例如“P1Y2M10DT2H30M”,以及附加上下文信息
Repeating intervals 重复间隔
根据ISO 8601,重复间隔Repeating Intervals是:
通过在区间表达式的开头添加”R[n]/”形成,其中R用作字母本身,[n]由重复次数代替。省略[n]的值意味着无限次数的重复。
例如,要从“2008-03-01T13:00:00Z”开始重复“P1Y2M10DT2H30M”的间隔五次,请使用“R5/2008-03-01T13:00:00Z/P1Y2M10DT2H30M”。
版本控制
所有符合Microsoft REST API指南的API都必须支持显式版本控制。客户可以指望服务随着时间的推移保持稳定至关重要,服务可以添加功能并进行更改也很重要。
版本控制格式
服务使用Major.Minor版本控制方案进行版本控制。服务可以选择仅“主要”版本方案,在这种情况下,“.0”是隐含的,并且本节中的所有其他规则都适用。支持用于指定REST API请求版本的两个选项:
- 嵌入在请求URL的路径中,在服务根目录的末尾:https://api.contoso.com/v1.0/products/users
- 作为URL的查询字符串参数:https://api.contoso.com/products/users?api-version=1.0
在这两个选项之间进行选择的指南如下:
- 位于DNS endpoint后面的服务必须使用相同的版本控制机制。
- 在这种情况下,跨端点的一致用户体验至关重要。Microsoft REST API指南工作组建议在未与组织的领导团队进行明确对话的情况下,不要创建新的顶级DNS endpoint。
- 保证其REST API的URL路径稳定性的服务,即使通过API的未来版本,也可以采用查询字符串参数机制。这意味着API中描述的关系的命名和结构在API发布后无法演变,即使在具有重大更改的版本之间也是如此。
- 无法确保未来版本中URL路径稳定性的服务必须将版本嵌入URL路径中。
某些基础服务(例如Microsoft的Azure Active Directory)可能会暴露在多个endpoint之后。此类服务必须支持每个endpoint的版本控制机制,即使这意味着支持多种版本控制机制。
组版本控制
组版本控制是一个可选功能,可以使用查询字符串参数机制在服务上提供。组版本允许在通用版本控制名称下对API endpoint进行逻辑分组。这允许开发人员查找单个版本号并跨多个端点使用它。组版本号是众所周知的,服务应该拒绝任何无法识别的值。
在内部,服务将采用组版本并将其映射到适当的Major.Minor版本。
组版本格式定义为YYYY-MM-DD,例如2012-12-07表示2012年12月7日。此日期版本格式仅适用于组版本,不应用作Major.Minor版本的替代。
组版本控制示例:
Group | Major.Minor |
2012-12-01 | 1.0 |
1.1 | |
1.2 | |
2013-03-21 | 1.0 |
2.0 | |
3.0 | |
3.1 | |
3.2 | |
3.3 |
Version Format | Example | Interpretation |
{groupVersion} | 2013-03-21, 2012-12-01 | 3.3, 1.2 |
{majorVersion} | 3 | 3.0 |
{majorVersion}.{minorVersion} | 1.2 | 1.2 |
客户端可以指定组版本或 Major.Minor 版本:
例如:
GET http://api.contoso.com/acct1/c1/blob2?api-version=1.0
PUT http://api.contoso.com/acct1/c1/b2?api-version=2011-12-07
什么时候更新版本
服务必须增加它们的版本号以响应任何中断的 API 更改。有关什么构成重大更改的详细讨论,请参阅以下部分。如果需要,服务也可以为不间断的更改增加它们的版本号。
使用新的主要版本号来表示将来会弃用对现有客户端的支持。在引入新的主要版本时,服务必须为现有客户提供清晰的升级路径,并制定与其业务组政策一致的弃用计划。服务应该对所有其他更改使用新的次要版本号。
版本化服务的在线文档必须指出每个先前 API 版本的当前支持状态,并提供最新版本的路径。
重大变更的定义
对 API 合同的更改被视为重大更改。影响 API 向后兼容性的更改是重大更改。
团队可以根据其业务需求定义向后兼容性。例如,Azure 将在响应中添加新的 JSON 字段定义为不向后兼容。Office 365 对向后兼容性有更宽松的定义,并允许将 JSON 字段添加到响应中。
重大更改的清晰示例:
- 删除或重命名 API 或 API 参数
- 现有 API 的行为变化
- 错误代码和错误合约的变化
- 任何违反最小惊讶原则 的事情
服务必须明确定义他们对重大更改的定义,尤其是在向 JSON 响应添加新字段和添加具有默认字段的新 API 参数方面。与其他服务位于 DNS endpoint 后面的服务必须在定义合同可扩展性方面保持一致。
OData V4 规范的本节 中描述的适用更改应被视为所有服务必须考虑的重大更改的最低标准的一部分。
长时间运行的操作
长时间运行的操作,有时也称为异步操作,对不同的人来说往往意味着不同的事情。本节针对不同类型的长时间运行操作提出了指导,并描述了这些类型操作的连线协议和最佳实践。
- 一个或多个客户端必须能够同时监控和操作同一资源。
- 系统状态应该始终是可发现和可测试的。即使操作跟踪资源不再处于活动状态,客户端也应该能够确定系统状态。查询长时间运行的操作状态的行为本身应该利用网络的原则。即具有统一接口语义的明确定义的资源。客户端可以对某些资源发出 GET 请求以确定长时间运行的操作的状态
- 长期运行的操作应该适用于希望”即发即忘”的客户以及希望积极监控结果并根据结果采取行动的客户。
- 取消并不明确表示回滚。在每个 API 定义的情况下,它可能意味着回滚、补偿、完成或部分完成等。在取消操作之后,客户不应该负责将服务返回到允许继续服务的一致状态。
基于资源的长时间运行操作 (RELO)
基于资源的建模是将操作的状态编码在资源中,并且使用的有线协议是标准同步协议。在这个模型中,状态转换被很好地定义,目标状态也被类似地定义。
这是长时间运行操作的首选模型,应尽可能使用。避免 LRO Wire Protocol 的复杂性和机制使我们的用户和工具链的事情变得更简单。
一个例子可能是机器重新启动,其中操作本身同步完成,但虚拟机资源上的 GET 操作将具有”状态:重新启动”、”状态:运行”,可以随时查询。
该模型可以集成推送通知。
虽然大多数操作可能是 POST 语义,但除了 POST 语义外,服务可以通过路由支持 PUT 语义以简化其 API。例如,想要创建名为”db1″的数据库的用户可以调用:
PUT https://api.contoso.com/v1.0/databases/db1
在这种情况下,数据库段正在处理 PUT 操作。
服务也可以使用下面定义的混合。
逐步长运行操作
逐步操作是一种需要很长且通常不可预测的时间才能完成的操作,并且不提供在资源中建模的状态转换。本节概述了服务应该用于公开此类长时间运行的操作的方法。
服务可以公开逐步操作。
逐步长时间运行的操作有时称为”异步”操作。这会导致混淆,因为它将平台元素(”Async/await”、”promises”、”futures”)与 API 操作元素混合在一起。本文档使用术语”逐步长时间运行操作”或通常只是”逐步操作”以避免混淆”异步”一词。
服务必须对逐步请求执行尽可能多的同步验证。服务必须以同步方式优先返回错误,目标是使用长时间运行的操作线协议处理”有效”操作。
对于定义为逐步长时间运行操作的 API,即使该操作可以立即完成,服务也必须经过逐步长时间运行操作流程。换句话说,API 必须采用并坚持 LRO 模式,而不是根据情况更改模式。
PUT
服务可以为实体创建启用 PUT 请求。
PUT https://api.contoso.com/v1.0/databases/db1
在这种情况下,数据库段正在处理 PUT 操作。
HTTP/1.1 202 Accepted Operation-Location: https://api.contoso.com/v1.0/operations/123
对于需要在此处返回 201 Created 的服务,请使用下面描述的混合流程。
202 Accepted 不应返回任何正文。201 Created 案例应该返回目标资源的主体。
POST
服务可以为实体创建启用 POST 请求。
POST https://api.contoso.com/v1.0/databases/ { "fileName": "someFile.db", "color": "red" } HTTP/1.1 202 Accepted Operation-Location: https://api.contoso.com/v1.0/operations/123
POST,混合模型
服务可以同步响应对创建资源的集合的 POST 请求,即使在生成响应时资源没有完全创建。为了使用这种模式,响应必须包括不完整资源的表示和它不完整的指示。
例如:
POST https://api.contoso.com/v1.0/databases/ HTTP/1.1 Host: api.contoso.com Content-Type: application/json Accept: application/json { "fileName": "someFile.db", "color": "red" }
服务响应表示数据库已创建,但通过包含 Operation-Location 标头指示请求未完成。在这种情况下,响应负载中的状态属性也指示操作尚未完全完成。
HTTP/1.1 201 Created Location: https://api.contoso.com/v1.0/databases/db1 Operation-Location: https://api.contoso.com/v1.0/operations/123 { "databaseName": "db1", "color": "red", "Status": "Provisioning", […other fields for "database"…] }
Operations resource
服务可以在租户级别提供“/operations”资源。
提供“/operations”资源的服务必须提供 GET 语义。GET 必须枚举操作集,遵循标准的分页、排序和过滤语义。此操作的默认排序顺序必须是:
初级分类 | 二级分类 |
未启动操作 | 操作创建时间 |
运行操作 | 操作创建时间 |
已完成的操作 | 操作创建时间 |
请注意,“完成的操作”是一个目标状态(见下文),实际上可能是几种不同状态中的任何一种,例如“成功”、“取消”、“失败”等。
Operation resource
操作是用户可寻址的资源,用于跟踪逐步长时间运行的操作。操作必须支持 GET 语义。针对操作的 GET 操作必须返回:
- 操作资源、它的状态以及与特定 API 相关的任何扩展状态。
- 200 OK 作为响应代码。
服务可以通过在操作上公开 DELETE 来支持操作取消。如果支持 DELETE 操作必须是幂等的。
注意:从 API 设计的角度来看,取消并不明确意味着回滚。在每个 API 定义的情况下,它可能意味着回滚、补偿、完成或部分完成等。在取消操作之后,客户不应该负责将服务返回到允许继续服务的一致状态。
不支持操作取消的服务必须在 DELETE 事件中返回 405 Method Not Allowed。
操作必须支持以下状态:
- 未开始
- 跑步
- 成功。终端状态。
- 失败。终端状态。
服务可以添加额外的状态,例如“取消”或“部分完成”。支持取消的服务必须充分描述它们的取消,以便可以准确地确定系统的状态,并且可以运行任何补偿操作。
支持附加状态的服务应考虑此规范名称列表,并尽可能避免创建新名称:取消、取消、中止、中止、墓碑、删除、已删除。
操作必须包含并在 GET 响应中提供以下信息:
- 操作创建时的时间戳。
- 进入当前状态的时间戳。
- 运行状态(未启动/正在运行/已完成)。
服务可以在操作中添加额外的、特定于 API 的字段。返回的操作状态 JSON 如下所示:
{ "createdDateTime": "2015-06-19T12-01-03.45Z", "lastActionDateTime": "2015-06-19T12-01-03.45Z", "status": "notstarted|running|succeeded|failed" }
Percent complete
有时,服务不可能准确地知道操作何时完成。这使得使用 Retry-After 标头有问题。在这种情况下,服务可以在 operationStatus JSON 中包含一个完成百分比字段。
{ "createdDateTime": "2015-06-19T12-01-03.45Z", "percentComplete": "50", "status": "running" }
在此示例中,服务器已向客户端指示长时间运行的操作已完成 50%。
Target resource location
对于导致或操纵资源的操作,服务必须在操作完成后的状态中包含目标资源位置。
{ "createdDateTime": "2015-06-19T12-01-03.45Z", "lastActionDateTime": "2015-06-19T12-06-03.0024Z", "status": "succeeded", "resourceLocation": "https://api.contoso.com/v1.0/databases/db1" }
Operation tombstones
服务可以选择支持逻辑删除操作。服务可以选择在服务定义的时间段后删除 tombstones。
The typical flow, polling
- 客户端通过使用 POST 调用操作来调用逐步操作
- 服务器必须通过响应 202 Accepted 状态码来表明请求已经开始。响应应该包括包含 URL 的位置标头,客户端在等待 Retry-After 标头中指定的秒数后应轮询结果。
- 客户端轮询位置,直到收到带有终端操作状态的 200 响应。
Example of the typical flow, polling
客户端调用重启动作:
POST https://api.contoso.com/v1.0/databases HTTP/1.1 Accept: application/json { "fromFile": "myFile.db", "color": "red" }
服务器响应表明请求已创建。
HTTP/1.1 202 Accepted Operation-Location: https://api.contoso.com/v1.0/operations/123
客户端等待一段时间然后调用另一个请求来尝试获取操作状态。
GET https://api.contoso.com/v1.0/operations/123 Accept: application/json
服务器响应结果仍未准备好并可选择提供等待 30 秒的建议。
HTTP/1.1 200 OK Retry-After: 30 { "createdDateTime": "2015-06-19T12-01-03.4Z", "status": "running" }
客户端等待建议的 30 秒,然后调用另一个请求以获取操作结果。
GET https://api.contoso.com/v1.0/operations/123 Accept: application/json
服务器以包含资源位置的“status: succeeded”操作响应。
HTTP/1.1 200 OK Content-Type: application/json { "createdDateTime": "2015-06-19T12-01-03.45Z", "lastActionDateTime": "2015-06-19T12-06-03.0024Z", "status": "succeeded", "resourceLocation": "https://api.contoso.com/v1.0/databases/db1" }
The typical flow, push notifications
- 客户端通过使用POST调用操作来调用长时间运行的操作。客户端已经在父资源上设置了推送通知。
- 服务通过响应202 Accepted状态代码来指示请求已启动。客户忽略其他一切。
- 整体操作完成后,服务通过对父资源的订阅推送通知。
- 客户端通过资源URL获取操作结果
Example of the typical flow, push notifications existing subscription
客户端调用备份操作。客户端已经为db1设置了推送通知订阅。
POST https://api.contoso.com/v1.0/databases/db1?backup HTTP/1.1 Accept: application/json
服务器响应表明请求已被接受。
HTTP/1.1 202 Accepted Operation-Location: https://api.contoso.com/v1.0/operations/123
调用者忽略返回中的所有标头。
操作完成后,目标URL会收到推送通知。
HTTP/1.1 200 OK Content-Type: application/json { "value": [ { "subscriptionId": "1234-5678-1111-2222", "context": "subscription context that was specified at setup", "resourceUrl": "https://api.contoso.com/v1.0/databases/db1", "userId": "contoso.com/user@contoso.com", "tenantId": "contoso.com" } ] }
Retry-After
在上面的示例中,Retry-After标头指示客户端在尝试从位置标头标识的URL获取结果之前应等待的秒数。
HTTP规范允许Retry-After标头交替指定HTTP日期,因此客户端也应准备好处理此问题
HTTP/1.1 202 Accepted Operation-Location: http://api.contoso.com/v1.0/operations/123 Retry-After: 60
注意:HTTP日期的使用与本文档中使用的ISO8601日期格式的使用不一致,但由[RFC7231][rfc-7231-7-1-1-1]中的HTTP标准明确定义。服务应该比HTTP日期格式更喜欢整数秒数(十进制)格式。
操作结果保留政策
在某些情况下,长时间运行的操作的结果不是可以寻址的资源。例如,如果您调用返回布尔值(而不是资源)的长时间运行的Action。在这些情况下,Location标头指向可以检索布尔结果的位置。
这就引出了一个问题:“操作结果应该保留多久?”
建议的最短保留时间为24小时。
操作应该在从系统中清除之前过渡到“tombstone”一段时间。
节流、配额和限制
原则
服务应尽可能响应,以免阻塞调用者。根据经验,任何预计在第99个百分位数中花费超过0.5秒的API调用都应考虑对这些调用使用长时间运行的操作模式。显然,面对来自调用者的潜在无限负载,服务无法保证这些响应时间。因此,服务应设计和记录客户端的调用请求限制,并在超出这些限制时以适当的、可操作的错误和错误消息进行响应。当服务通常过载时,服务应该快速响应错误,而不是简单地缓慢响应。最后,许多服务都会有呼叫配额,可能是每小时或每天的操作次数,通常与服务计划或价格有关。当超过这些配额时,服务还必须立即提供可操作的错误。配额和限制应适用于客户单位:订阅、租户、应用程序、计划,或者没有任何其他标识的IP地址范围……适合服务目标,以便正确共享负载和一个单位不干扰他人。
返回代码(429与503)
HTTP为这些场景指定了两个返回码:“429 Too Many Requests”和“503 Service Unavailable”。对于客户端调用过多的情况,服务应该使用429,并且可以通过更改其调用模式来解决这种情况。如果一般负载或个别调用者无法控制的其他问题导致服务变慢,则服务应响应503。在所有情况下,服务还应提供建议呼叫者在再次尝试之前应等待多长时间的信息。客户端应尊重这些标头,并实施其他瞬态故障处理技术。但是,可能有些客户端会在失败时立即重试,这可能会增加服务的负载。为了解决这个问题,服务应该设计为尽可能便宜地返回429或503,或者通过放入特殊的快速路径代码,或者理想情况下通过依赖提供此功能的通用前门或负载平衡器。
Retry-After和RateLimit标头
Retry-After标头是响应受限制的客户端的标准方式。在限制和配额(但不是整体系统负载)的情况下,使用描述超出限制的标头响应也是常见的,但可选的。但是,Microsoft和整个行业的服务为此使用了各种不同的标头。我们建议使用三个标头来描述限制、低于限制的剩余调用次数以及限制重置的时间。但是,其他标题可能适用于特定类型的限制。在所有情况下,这些都必须记录在案。
服务指南
服务应选择适合SLA或业务目标的时间窗口。在Quotas的情况下,Retry-After时间和时间窗口可能很长(小时、天、周、甚至几个月。服务使用429表示特定调用者拨打了太多电话,而503表示服务正在减载,但这不是调用者的责任。
响应能力
- 服务必须在所有情况下都快速响应,即使在负载下也是如此。
- 在第99个百分位数内响应时间超过1s的调用应该使用Long-Running Operation模式
- 在第99个百分位数内响应时间超过5秒的呼叫应强烈考虑LRO模式
- 服务不应该引入阻塞调用者或不可操作的睡眠、暂停等(“tar-pitting”)。
速率限制和配额
当请求者发起太多请求时
- 服务必须返回429代码
- 服务必须返回描述细节的标准错误响应,以便程序员可以进行适当的更改
- 服务必须返回一个Retry-After标头,指示客户端在重试之前应该等待多长时间
- 服务可以返回RateLimit标头,记录已超出的限制或配额
- 服务可以返回RateLimit-Limit:客户端在一个时间窗口内允许进行的调用次数
- 服务可以返回RateLimit-Remaining:时间窗口内剩余的调用次数
- 服务可以返回RateLimit-Reset:窗口重置的时间,以UTC纪元秒为单位
- 服务可以返回其他服务特定的RateLimit标头以获取更详细的信息或特定的限制或配额
服务超载
当服务普遍过载和减载时
- 服务必须返回503代码
- 服务必须返回描述细节的标准错误响应(见10.2),以便程序员可以进行适当的更改
- 服务必须返回一个Retry-After标头,指示客户端在重试之前应该等待多长时间
- 在503的情况下,服务不应该返回RateLimit标头
响应示例
HTTP/1.1 429 Too Many Requests Content-Type: application/json Retry-After: 5 RateLimit-Limit: 1000 RateLimit-Remaining: 0 RateLimit-Reset: 1538152773 { "error": { "code": "requestLimitExceeded", "message": "The caller has made too many requests in the time period.", "details": { "code": "RateLimit", "limit": "1000", "remaining": "0", "reset": "1538152773", } } }
请求指南
调用者包括API的所有用户:工具、门户、其他服务,而不仅仅是用户客户端
- 在重试请求之前,调用者必须等待带有 Retry-After 的响应中指示的最短时间。
- 调用者可以假设在收到带有 Retry-After 标头的响应后请求是可重试的,而不对请求进行任何更改。
- 客户端应该使用共享的 SDK 和常见的瞬时故障库来实现正确的行为
处理忽略 Retry-After 标头的调用者
理想情况下,429 和 503 返回的成本非常低,甚至可以处理立即重试的客户端。在这些情况下,如果可能,服务团队应努力联系或修复客户。如果它是已知的合作伙伴,则应提交错误或事件。在极端情况下,可能需要使用 DoS 风格的保护措施,例如阻止请求者。
通过 Webhook 推送通知
范围
服务可以通过 WebHook 实现推送通知。本节讨论以下关键场景:
通过 HTTP 回调(通常称为 WebHook)将通知推送到可公开寻址的服务器。
之所以选择所提出的方法,是因为它简单、适用性广、服务订阅者进入门槛低。它旨在作为最低要求集和附加功能的起点。
原则
支持 webhook 的服务的核心原则是:
- 服务必须至少实现一个 poke/pull 模型。在 poke/pull 模型中,向客户端发送通知,然后客户端发送请求以获取当前状态或自上次通知以来的更改记录。这种方法避免了消息排序、遗漏消息和变更集的复杂性。服务可以添加更多数据以提供丰富的通知。
- 服务必须实现挑战/响应协议来配置回调 URL。
- 服务应该有一个推荐的老化期,服务可以根据场景灵活变化。
- 服务应该允许发出成功通知的订阅永远存在,并且应该容忍合理的中断期。
- Firehose 订阅必须仅通过 HTTPS 交付。服务应该要求其他订阅类型为 HTTPS。有关更多详细信息,请参阅“安全”部分。
订阅类型
有两种订阅类型,服务可以实现,两者之一,或不实现。支持的订阅类型有:
- Firehose 订阅 – 为订阅应用程序手动创建订阅,通常在应用程序注册门户中。任何用户同意应用程序接收的活动通知都会发送到这个单一订阅。
- 按资源订阅——订阅应用程序使用代码在运行时以编程方式为某些特定于用户的实体创建订阅。
支持两种订阅类型的服务应该为这两种类型提供差异化的开发者体验:
- Firehose – 除了直接验证和响应通知之外,服务不得要求开发人员创建代码。服务必须为订阅管理提供管理 UI。服务不应该假设最终用户知道订阅,只知道订阅应用程序的功能。
- 每用户 – 服务必须为开发人员提供 API 以创建和管理订阅作为其应用程序的一部分以及验证和响应通知。服务可能希望最终用户知道订阅,并且必须允许最终用户在响应用户操作时直接撤销订阅。
调用序列
Firehose 订阅的调用顺序必须遵循下图。它显示了应用程序和订阅的手动注册,然后是最终用户使用服务的 API 之一。在流程的这一部分,必须存储两件事:
- 服务必须存储最终用户同意从这个特定应用程序接收通知的行为(通常是后台使用 OAUTH 范围。)
- 订阅应用程序必须存储最终用户的令牌,以便在收到更改通知后回调详细信息。
序列的最后一部分是通知流本身。
非规范实施指南:服务中的一个资源发生变化,服务需要运行如下逻辑:
- 确定有权访问资源的用户集,因此可以期望应用程序代表他们接收有关它的通知。
- 查看哪些用户已同意接收通知以及来自哪些应用程序。
- 查看哪些应用程序注册了 Firehose 订阅。
- 加入 1、2、3 以生成必须发送给应用程序的具体通知集。
应该注意的是,用户同意的行为和设置 Firehose 订阅的行为可以按任一顺序到达。服务应该以任一顺序处理设置发送通知。
对于每用户订阅,应用程序注册要么是手动的,要么是自动的。每个用户订阅的调用流程必须遵循下图。它显示最终用户使用服务的 API 之一,同样,必须存储相同的两件事:
- 服务必须存储最终用户同意从这个特定应用程序接收通知的行为(通常是后台使用 OAUTH 范围)。
- 订阅应用程序必须存储最终用户的令牌,以便在收到更改通知后回调详细信息。
在这种情况下,订阅是使用订阅应用程序中的最终用户令牌以编程方式设置的。应用程序必须将注册订阅的 ID 与用户令牌一起存储。
非规范实施指南:在序列的最后部分,当服务中的一项数据发生变化并且服务需要运行以下逻辑时:
- 查找通过资源与更改的数据对应的订阅集。
- 对于在 app+user 令牌下创建的订阅,使用订阅创建者的订阅 ID 和用户 ID 向每个订阅发送通知。
对于使用仅限应用程序令牌创建的订阅,检查更改数据的所有者或任何可以查看更改数据的用户是否已同意向应用程序发送通知,如果同意,则向应用程序发送一组每个用户 ID 的通知每个带有订阅 ID 的订阅。
验证订阅
当订阅以编程方式更改或通过管理 UI 门户响应更改时,需要保护订阅服务免受来自可能推送大量通知流量的服务的恶意或意外调用。
对于所有订阅,无论是 firehose 还是每个用户,在发送任何其他通知之前,服务必须通过门户 UI 或 API 请求发送验证请求作为创建或修改的一部分。
验证请求必须采用以下格式作为对订阅的 notificationUrl 的 HTTP/HTTPS POST。
POST https://{notificationUrl}?validationToken={randomString} ClientState: clientOriginatedOpaqueToken (if provided by client on subscription-creation) Content-Length: 0
对于要建立的订阅,应用程序必须以 200 OK 响应此请求,并使用 validationToken 值作为唯一的实体主体。请注意,如果 notificationUrl 包含查询参数,则 validationToken 参数必须附加一个 &。
如果任何质询请求在发送请求后 5 秒内未收到规定的响应,则服务必须返回错误,不得创建订阅,并且不得向 notificationUrl 发送进一步的请求或通知。
服务可以对 URL 所有权执行额外的验证。
接收通知
服务应该发送通知以响应服务数据更改,这些更改不包括更改本身的详细信息,但包含足够的信息供订阅应用程序适当地响应以下过程:
- 应用程序必须识别正确的缓存 OAuth 令牌以用于回调
- 应用程序可以查找任何先前的 delta 代币以获取相关的更改范围
- 应用程序必须确定要调用的 URL,以便为服务的新状态执行相关查询,这可以是增量查询。
提供将被转发给最终用户的通知的服务可以选择向通知数据包添加更多详细信息,以减少其服务上的传入呼叫负载。此类服务必须明确通知不能保证交付,并且可能会丢失或出现故障。
通知可以汇总并分批发送。应用程序必须准备好在单个推送通知中接收多个事件。
服务必须将所有 WebHook 数据通知作为 POST 请求发送。
服务必须允许 30 秒的通知超时。如果发生超时或应用程序以 5xx 响应响应,则服务应该使用指数退避重试通知。所有其他响应将被忽略。
该服务不得遵循 301/302 重定向请求。
Notification payload
通知有效 payload 的基本格式是一个事件列表,每个事件包含引用资源发生更改的订阅的 ID、更改类型、应该消耗的资源以识别更改的确切细节以及足够的身份信息来查看调用该资源所需的令牌。
对于 Firehose 订阅,一个具体的例子可能如下所示:
{ "value":[ { "subscriptionId":"32b8cbd6174ab18b", "resource":"https://api.contoso.com/v1.0/users/user@contoso.com/files?$delta", "userId":"<UserGUID>", "tenantId":"<TenantId>" } ] }
对于每用户订阅,一个具体的例子可能如下所示:
{ "value":[ { "subscriptionId":"32b8cbd6174ab183", "clientState":"clientOriginatedOpaqueToken", "expirationDateTime":"2016-02-04T11:23Z", "resource":"https://api.contoso.com/v1.0/users/user@contoso.com/files/$delta", "userId":"<UserGUID>", "tenantId":"<TenantId>" }, { "subscriptionId":"97b391179fa22", "clientState":"clientOriginatedOpaqueToken", "expirationDateTime":"2016-02-04T11:23Z", "resource":"https://api.contoso.com/v1.0/users/user@contoso.com/files/$delta", "userId":"<UserGUID>", "tenantId":"<TenantId>" } ] }
以下是 JSON 有效 payload 的详细说明。
通知项包含一个顶级对象,该对象包含一系列事件,每个事件都标识了发送此通知的订阅。
Field | Description |
value | 自上次通知以来在订阅范围内引发的事件数组。 |
events 数组的每一项都包含以下属性:
Field | Description |
subscriptionId | 已发送此通知的订阅 ID。
服务必须提供 subscriptionId 字段。 |
clientState | 如果在订阅创建时提供,服务必须提供 clientState 字段。 |
expirationDateTime | 如果订阅有一个,服务必须提供 expireDateTime 字段。 |
resource | 服务必须提供资源字段。这个 URL 必须被订阅应用程序认为是不透明的。在更丰富的通知的情况下,它可以被隐含地包含资源 URL 的消息内容包含以避免重复。
如果服务将这些数据作为更详细数据包的一部分提供,则不需要复制它。 |
userId | 服务必须为用户范围的资源提供这个字段。对于用户范围的资源,应使用用户的唯一标识符。
在一组特定用户之间共享资源的情况下,必须发送多个通知,传递每个用户的唯一标识符。 对于租户范围的资源,应使用订阅的用户 ID。 |
tenantId | 希望支持跨租户请求的服务应该提供这个字段。提供租户范围数据通知的服务必须发送此字段。 |
以编程方式管理订阅
对于每用户订阅,必须提供 API 来创建和管理订阅。API 必须至少支持此处描述的操作。
创建订阅
客户端通过对订阅资源发出 POST 请求来创建订阅。订阅命名空间是客户端通过 POST 操作定义的。
https://api.contoso.com/apiVersion/$subscriptions
POST 请求包含要创建的单个订阅对象。该订阅对象具有以下属性:
PropertyName | Required | Notes |
resource | Yes | 要观看的资源路径。 |
notificationUrl | Yes | The target webhook URL. |
clientState | No | 在所有通知中传递回客户端的不透明字符串。调用者可以选择使用它来提供标记机制。 |
如果订阅成功创建,服务必须响应状态代码 201 CREATED 和至少包含以下属性的主体:
PropertyName | Required | Notes |
id | Yes | 新订阅的唯一 ID,稍后可用于更新/删除订阅。 |
expirationDateTime | No | 使用现有的 Microsoft REST API 指南定义的时间格式。 |
订阅的创建应该是幂等的。范围限定为身份验证令牌的属性组合提供了唯一性约束。
以下是使用用户+应用程序主体订阅来自文件的通知的示例请求:
POST https://api.contoso.com/files/v1.0/$subscriptions HTTP 1.1 Authorization: Bearer {UserPrincipalBearerToken} { "resource":"http://api.service.com/v1.0/files/file1.txt", "notificationUrl":"https://contoso.com/myCallbacks", "clientState":"clientOriginatedOpaqueToken" }
服务应该以最小的响应格式响应这样的消息:
{ "id":"32b8cbd6174ab18b", "expirationDateTime":"2016-02-04T11:23Z" }
下面是一个使用 Application-Only 主体的示例,其中应用程序正在监视它授权的所有文件:
POST https://api.contoso.com/files/v1.0/$subscriptions HTTP 1.1 Authorization: Bearer {ApplicationPrincipalBearerToken} { "resource": "All.Files", "notificationUrl": "https://contoso.com/myCallbacks", "clientState": "clientOriginatedOpaqueToken" }
服务应该以最小的响应格式响应这样的消息:
{ "id": "8cbd6174abb391179", "expirationDateTime": "2016-02-04T11:23Z" }
更新订阅
服务可以支持修改订阅。要更新现有订阅的属性,客户端使用 PATCH 请求提供 ID 和需要更改的属性。省略的属性将保留其值。要删除属性,请为其分配 JSON null 值。
与创建一样,订阅是单独管理的。
以下请求更改现有订阅的通知 URL:
PATCH https://api.contoso.com/files/v1.0/$subscriptions/{id} HTTP 1.1 Authorization: Bearer {UserPrincipalBearerToken} { "notificationUrl": "https://contoso.com/myNewCallback" }
如果 PATCH 请求包含新的 notificationUrl,则服务器必须如上所述对其进行验证。如果新 URL 无法验证,则服务必须使 PATCH 请求失败并将订阅保留在其先前的状态。
服务必须返回一个空的 body 和 204 No Content 来指示一个成功的补丁。
如果补丁失败,服务必须返回错误正文和状态代码。
操作必须以原子方式成功或失败。
删除订阅
服务必须支持删除订阅。可以通过对订阅资源发出 DELETE 请求来删除现有订阅:
DELETE https://api.contoso.com/files/v1.0/$subscriptions/{id} HTTP 1.1 Authorization: Bearer {UserPrincipalBearerToken}
与更新一样,服务必须返回 204 No Content 以表示成功删除,或返回错误正文和状态代码以指示失败。
枚举订阅
要获取活动订阅列表,客户端使用 User+Application 或 Application-Only 承载令牌针对订阅资源发出 GET 请求:
GET https://api.contoso.com/files/v1.0/$subscriptions HTTP 1.1 Authorization: Bearer {UserPrincipalBearerToken}
服务必须使用用户+应用程序主体承载令牌返回如下格式:
{ "value": [ { "id": "32b8cbd6174ab18b", "resource": "http://api.contoso.com/v1.0/files/file1.txt", "notificationUrl": "https://contoso.com/myCallbacks", "clientState": "clientOriginatedOpaqueToken", "expirationDateTime": "2016-02-04T11:23Z" } ] }
一个可能使用 Application-Only 主体不记名令牌返回的示例:
{ "value": [ { "id": "6174ab18bfa22", "resource": "All.Files", "notificationUrl": "https://contoso.com/myCallbacks", "clientState": "clientOriginatedOpaqueToken", "expirationDateTime": "2016-02-04T11:23Z" } ] }
安全
所有服务 URL 都必须是 HTTPS(即所有入站调用都必须是 HTTPS)。处理 WebHook 的服务必须接受 HTTPS。
我们建议允许客户端定义的 WebHook 回调 URL 的服务不应通过 HTTP 传输数据。这是因为信息可能会通过客户端、网络、服务器日志和其他机制无意中暴露出来。
但是,在某些情况下,由于客户端端点或软件限制,无法遵循上述建议。因此,服务可能允许 HTTP 的 web 钩子 URL。
此外,允许客户端定义的 HTTP Web 钩子回调 URL 的服务应该符合工程领导指定的隐私政策。这通常包括建议客户端首选 SSL 连接并遵守特殊预防措施以确保正确处理日志和其他服务数据收集。
例如,服务可能不想要求开发人员生成证书以供使用。服务可能仅在测试帐户上启用此功能。
不支持的请求
RESTful API 客户端可以请求当前不受支持的功能。RESTful API 必须响应与本节一致的有效但不受支持的请求。
基本原则
RESTful API 通常会选择限制客户端可以执行的功能。例如,审计系统允许创建记录但不允许修改或删除记录。类似地,一些 API 将公开集合但需要或以其他方式限制过滤和排序标准,或者可能不支持客户端驱动的分页。
功能允许列表
如果服务不支持以下任何 API 功能,并且调用者请求该功能,则必须提供错误响应。特点是:
- 集合中的关键寻址,例如:https://api.contoso.com/v1.0/people/user1@contoso.com
- 按属性值过滤集合,例如:https://api.contoso.com/v1.0/people?$filter=name eq ‘david’
- 按范围过滤集合,例如:http://api.contoso.com/v1.0/people?$filter=hireDate ge 2014-01-01 和 hiringDate le 2014-12-31
- 通过 $top 和 $skip 进行客户端驱动的分页,例如:http://api.contoso.com/v1.0/people?$top=5&$skip=2
- 按 $orderBy 排序,例如:https://api.contoso.com/v1.0/people?$orderBy=name desc
- 提供 $delta 令牌,例如:https://api.contoso.com/v1.0/people?$delta
错误响应
如果调用者请求在功能允许列表中发现不受支持的功能,服务必须提供错误响应。错误响应必须是来自 4xx 系列的 HTTP 状态代码,指示无法满足请求。除非更具体的错误状态适用于给定的请求,否则服务应该返回“400 Bad Request”和符合 Microsoft REST API 指南中提供的错误响应指南的错误负载。服务应该在响应消息中包含足够的细节,以便开发人员准确确定不支持请求的哪一部分。
例子:
GET https://api.contoso.com/v1.0/people?$orderBy=name HTTP/1.1 Accept: application/json HTTP/1.1 400 Bad Request Content-Type: application/json { "error": { "code": "ErrorUnsupportedOrderBy", "message": "Ordering by name is not supported." } }
命名指南
方法
命名策略应该帮助开发人员发现功能,而不必经常参考文档。通用模式和标准约定的使用极大地帮助开发人员正确猜测通用属性名称和含义。服务应该使用详细的命名模式,并且不应该使用除首字母缩略词以外的缩写词,这些缩写词是由 API 表示的域中的主要表达模式(例如 Url)。
Casing
- 首字母缩略词应该遵循大小写约定,就好像它们是常规单词一样(例如 Url)。
- 所有标识符,包括命名空间、entityType、entitySet、属性、动作、函数和枚举值,都应该使用小驼峰命名法。
- HTTP 标头是例外,应该使用 Capitalized-Hyphenated-Terms 大写-连字符的标准 HTTP 约定。
避免的名字
某些名称在 API 域中如此重载,以至于它们失去了所有意义,或者与使用 REST API 时无法避免的域中的其他常见用法(例如 OAUTH)发生冲突。服务不应使用以下名称:
- Context
- Scope
- Resource
Forming compound names
- 服务应避免使用诸如“a”、“the”、“of”之类的冠词,除非需要传达含义。
- 例如,不应使用诸如 aUser、theAccount、countOfBooks 之类的名称,而应首选 user、account、bookCount。
- 服务应该向属性名称添加类型,否则会导致数据表示方式的歧义或会导致服务不使用公共属性名称。
- 向属性名称添加类型时,服务必须在末尾添加类型,例如创建日期时间。
身份属性
- 服务必须使用字符串类型作为身份属性。
- 对于 OData 服务,服务必须使用 OData @id 属性来表示资源的规范标识符。
- 服务可以使用简单的“id”属性来表示资源的本地或遗留主键值。
- 服务应该使用后缀为“Id”的关系名称来表示另一个资源的外键,例如订阅 ID。
- 该属性的内容应该是引用资源的规范 ID。
日期和时间属性
- 对于需要日期和时间的属性,服务必须使用后缀“DateTime”。
- 对于只需要日期信息而不指定时间的属性,服务必须使用后缀“Date”,例如 birthDate。
- 对于只需要时间信息而不指定日期的属性,服务必须使用后缀“Time”,例如 appointmentStartTime。
名称属性
- 对于通常向用户显示的资源的整体名称,服务必须使用属性名称“displayName”。
- 服务可以使用其他常见的命名属性,例如,givenName, surname, signInName.
集合和计数
- 服务必须使用正确的英语将集合命名为复数名词或复数名词短语。
- 服务可以使用简单的英语来表示复数不常用的名词。
- 例如,可以使用 schemas 代替 schemata。
- 服务必须以名词或以“Count”为后缀的名词短语来命名资源计数。
常用属性名称
如果服务具有属性,其数据与下面的名称匹配,则服务必须使用此表中的名称。该表将随着服务添加更常用的术语而增长。添加此类条款的服务所有者应建议对本文档进行添加。
attendees |
body |
createdDateTime |
childCount |
children |
contentUrl |
country |
createdBy |
displayName |
errorUrl |
eTag |
event |
expirationDateTime |
givenName |
jobTitle |
kind |
id |
lastModifiedDateTime |
location |
memberOf |
message |
name |
owner |
people |
person |
postalCode |
photo |
preferredLanguage |
properties |
signInName |
surname |
tags |
userPrincipalName |
webUrl |
附录
时序图注释
本文档中的所有序列图均使用WebSequenceDiagrams.com 生成。要生成它们,请将下面的文本粘贴到 Web 工具中。
Push notifications, per user flow
===BeginText=== noteover Developer, Automation, AppServer: An App Developer like Movie Maker Wants to integrate with primary service like Dropbox end note noteover DBPortal, DBAppRegistration, DBNotifications, DBAuth, DBService: The primary service like Dropbox noteover Client: The end users' browser or installed app noteover Developer, Automation, AppServer, DBPortal, DBAppRegistration, DBNotifications, Client: Manual App Registration Developer<--> DBPortal: Login into Portal, App Registration UX DBPortal ->+ DBAppRegistration: App Name etc. noteover DBAppRegistration: Confirm Portal Access Token DBAppRegistration ->- DBPortal: App ID DBPortal<--> AppServer: Developer copies App ID noteover Developer, Automation, AppServer, DBPortal, DBAppRegistration, DBNotifications, Client: Manual Notification Registration Developer<--> DBPortal: webhook registration UX DBPortal ->+ DBNotifications: Register: AppServer webhook URL, Scope, App ID Noteover DBNotifications: Confirm Portal Access Token DBNotifications ->- DBPortal: notification ID DBPortal --> AppServer: Developer may copy notification ID noteover Developer, Automation, AppServer, DBPortal, DBAppRegistration, DBNotifications, Client: Client Authorization Client ->+ AppServer: Request access to DB protected information AppServer ->- Client: Redirect to DB Authorization endpoint with authorization request Client ->+ DBAuth: Redirected authorization request Client<--> DBAuth: Authorization UX DBAuth ->- Client: Redirect back to AppServer with code Client ->+ AppServer: Redirect request back to access server with access code AppServer ->+ DBAuth: Request tokens with access code note right of DBService: Cache that this User ID provided access to App ID DBAuth ->- AppServer: Response with access, refresh, and ID tokens note right of AppServer: Cache tokens by user ID AppServer ->- Client: Return information to client noteover Developer, Automation, AppServer, DBPortal, DBAppRegistration, DBNotifications, Client: Notification Flow Client<--> DBService: Changes to user data - typical via interacting with AppServer via Client DBService -> AppServer: Notification with notification ID and user ID AppServer ->+ DBService: Request changed information with cached access tokens and "since" token noteover DBService: Confirm User Access Token DBService ->- AppServer: Response with data and new "since" token note right of AppServer: Update status and cache new "since" token ===EndText===
Push notifications, firehose flow
===BeginText=== noteover Developer, Automation, AppServer: An App Developer like MovieMaker Wants to integrate with primary service like Dropbox end note noteover DBPortal, DBAppRegistration, DBNotifications, DBAuth, DBService: The primary service like Dropbox noteover Client: The end users' browser or installed app noteover Developer, Automation, AppServer, DBPortal, DBAppRegistration, DBNotifications, Client: App Registration alt Automated app registration Developer<--> Automation: Configure Automation ->+ DBAppRegistration: AppName etc. noteover DBAppRegistration: Confirm App Access Token DBAppRegistration ->- Automation: AppID, AppSecret Automation --> AppServer: Embed AppID, AppSecret else Manual app registration Developer<--> DBPortal: Login into Portal, App Registration UX DBPortal ->+ DBAppRegistration: AppName etc. noteover DBAppRegistration: Confirm Portal Access Token DBAppRegistration ->- DBPortal: AppID DBPortal<--> AppServer: Developer copies AppID end noteover Developer, Automation, AppServer, DBPortal, DBAppRegistration, DBNotifications, Client: Client Authorization Client ->+ AppServer: Request access to DB protected information AppServer ->- Client: Redirect to DBAuthorization endpoint with authorization request Client ->+ DBAuth: Redirected authorization request Client<--> DBAuth: Authorization UX DBAuth ->- Client: Redirect back to AppServer with code Client ->+ AppServer: Redirect request back to access server with access code AppServer ->+ DBAuth: Request tokens with access code note right of DBService: Cache that this UserID provided access to AppID DBAuth ->- AppServer: Response with access, refresh, and ID tokens note right of AppServer: Cache tokens by userID AppServer ->- Client: Return information to client noteover Developer, Automation, AppServer, DBPortal, DBAppRegistration, DBNotifications, Client: Notification Registration AppServer ->+ DBNotifications: Register: App server webhook URL, Scope, AppID noteover DBNotifications: Confirm User Access Token DBNotifications ->- AppServer: notification ID note right of AppServer: Cache the NotificationID and UserAccessToken noteover Developer, Automation, AppServer, DBPortal, DBAppRegistration, DBNotifications, Client: Notification Flow Client<--> DBService: Changes to user data - typical via interacting with AppServer via Client DBService -> AppServer: Notification with notificationID and userID AppServer ->+ DBService: Request changed information with cached access tokens and "since" token noteover DBService: Confirm User Access Token DBService ->- AppServer: Response with data and new "since" token note right of AppServer: Update status and cache new "since" token ===EndText===
参考链接: