33

在阅读了大量有关 REST 版本控制的材料后,我正在考虑对调用而不是 API 进行版本控制。例如:

http://api.mydomain.com/callfoo/v2.0/param1/param2/param3
http://api.mydomain.com/verifyfoo/v1.0/param1/param2

而不是首先拥有

http://api.mydomain.com/v1.0/callfoo/param1/param2
http://api.mydomain.com/v1.0/verifyfoo/param1/param2

然后去

http://api.mydomain.com/v2.0/callfoo/param1/param2/param3
http://api.mydomain.com/v2.0/verifyfoo/param1/param2

我看到的优势是:

  • 当调用发生变化时,我不必重写我的整个客户端——只需要重写受更改调用影响的部分。
  • 客户端的那些部分可以继续工作(我们投入了大量的测试时间来确保客户端和服务器端都是稳定的。)
  • 对于已更改的呼叫,我可以使用永久或非永久重定向。
  • 向后兼容将是一件轻而易举的事,因为我可以保留旧的呼叫版本。

我错过了什么吗?请指教。

4

6 回答 6

64

需要 HTTP 标头。

Version: 1

Version标头在RFC 4229中临时注册,并且有一些合理的理由避免使用 X- 前缀或特定于使用的 URI。yfeldblumhttps://stackoverflow.com/a/2028664提出了一个更典型的标头:

X-API-Version: 1

在任何一种情况下,如果标头丢失或与服务器可以交付的内容不匹配,请发送412 Precondition Failed响应代码以及失败的原因。这要求客户端每次都指定它们支持的版本,但会在客户端和服务器之间强制执行一致的响应。(可选地支持?version=查询参数会给客户额外的灵活性。)

这种方法简单、易于实施且符合标准。

备择方案

我知道一些非常聪明、善意的人建议使用 URL 版本控制和内容协商。在某些情况下和通常提出的形式,两者都存在重大问题。

URL 版本控制

如果您控制所有服务器和客户端,则端点/服务 URL 版本控制有效。否则,您将需要处理回退到旧服务器的较新客户端,最终您将使用自定义 HTTP 标头进行处理,因为部署在您无法控制的异构服务器上的服务器软件的系统管理员可以做各种各样的事情来搞砸如果您使用302 Moved Temporarily之类的东西,您认为很容易解析的 URL 。

内容协商

Accept如果您非常关心遵循 HTTP 标准但又想忽略 HTTP/1.1 标准文档的实际内容,则通过标头进行内容协商是可行的。您倾向于看到的建议的 MIME 类型是某种形式application/vnd.example.v1+json。有几个问题:

  1. 当然,在某些情况下,供应商扩展实际上是合适的,但是客户端和服务器之间略有不同的通信行为并不真正符合新“媒体类型”的定义。此外,RFC 2616 (HTTP/1.1)写道,“媒体类型值已在 Internet 号码分配机构注册。媒体类型注册过程在 RFC 1590 中进行了概述。不鼓励使用未注册的媒体类型。” 我不想为每个具有 REST API 的软件产品的每个版本都看到单独的媒体类型。
  2. 任何子类型范围(例如,application/*)都没有意义。对于将结构化数据返回给客户端进行处理和格式化的 REST API,接受有什么好处*/*
  3. Accept头需要一些努力才能正确解析。应该遵循隐含和明确的优先级,以最大程度地减少实际正确进行内容协商所需的来回。如果您担心正确实施此标准,那么正确执行这一点很重要。
  4. RFC 2616 (HTTP/1.1)描述了任何不包含Accept标头的客户端的行为:“如果不存在 Accept 标头字段,则假定客户端接受所有媒体类型。” 因此,对于您不自己编写的客户端(您控制最少的地方),最正确的做法是使用服务器知道的最新、最容易破坏的旧版本来响应请求关于。换句话说,您可能根本没有实现版本控制,而这些客户端仍然会以完全相同的方式中断。

2014 年编辑

我已经阅读了很多其他答案和每个人的深思熟虑的评论;我希望我可以利用几年的反馈改进这一点:

  1. 不要使用“X-”前缀。我认为Accept-Version2014 年可能更有意义,并且在评论中提出了关于重用语义的一些有效担忧Version。与定义的标头(例如)和 URI 的相对不透明性肯定有重叠Content-Version,我尽量小心地将两者与内容协商的变体混淆,Version标头实际上是。URL 的第三个“版本”与https://example.com/api/212315c2-668d-11e4-80c7-20c9d048772b“第二个”完全不同,无论它是否包含数据或文档。
  2. 关于我上面所说的关于 URL 版本控制的内容(例如https://example.com/v1/users,端点),反过来可能更符合事实:如果您控制所有服务器和客户端,那么 URL/URI 版本控制可能就是您想要的。对于可以发布单个服务 URL 的大型服务,我会为每个版本使用不同的端点,就像大多数. 我的特别看法深受以下事实的影响:上述实现通常由许多不同的组织部署在许多不同的服务器上,也许最重要的是,部署在我无法控制的服务器上。我总是想要一个规范的服务 URL,如果一个站点仍在运行 API 的 v3 版本,我绝对不希望请求https://example.com/v4/返回他们的网络服务器的404 Not Found页面(或者更糟糕的是,200 OK将他们的主页作为 500k 的 HTML 通过蜂窝数据返回给 iPhone 应用程序。)
  3. 如果您想要非常简单的 /client/ 实现(以及更广泛的采用),那么很难说在 HTTP 请求中要求自定义标头对于客户端作者来说就像GET-ting vanilla URL 一样简单。(尽管身份验证通常需要您的令牌或凭据在标头中传递,无论如何。使用VersionAccept-Version作为秘密握手以及实际的秘密握手非常适合。)
  4. 使用标头进行内容协商Accept有利于为相同内容获取不同的 MIME 类型(例如,XML、JSON 和 Adob​​e PDF),但没有为这些东西的版本定义(Dublin Core 1.1 vs. JSONP vs. PDF/A) . 如果您希望支持Accept标头,因为尊重行业标准很重要,那么您不希望虚构的 MIME 类型干扰您可能需要在请求中使用的媒体类型协商。保证定制的 API 版本标头不会干扰大量使用、经常引用的Accept,而将它们混为相同的用法只会让服务器和客户端感到困惑。也就是说,根据 2013 年的RFC6906将您期望的内容命名为命名配置文件由于很多原因,它比单独的标题更可取。这很聪明,我认为人们应该认真考虑这种方法
  5. 为每个请求添加标头是在无状态协议中工作的一个特殊缺点。
  6. 恶意代理服务器几乎可以做任何事情来破坏 HTTP 请求和响应。他们不应该,虽然我没有在这种情况下谈论Cache-ControlVary标头,但所有服务创建者都应该仔细考虑他们的内容在许多不同的环境中是如何被使用的。
于 2012-08-12T16:20:14.813 回答
17

这是一个见仁见智的问题;这是我的,以及意见背后的动机。

  1. 在 URL 中包含版本。
    对于那些说它属于 HTTP 标头的人,我说:也许。但是根据该领域的早期领导者,输入 URL 是公认的方法。(谷歌、雅虎、推特等)。这是开发人员所期望的,做开发人员所期望的,换句话说,按照最小惊讶的原则行事,可能是一个好主意。它绝对不会让“客户更难升级”。如果 URL 的更改以某种方式对消费应用程序的开发人员构成了障碍,正如此处不同答案中所建议的那样,则需要解雇该开发人员。

  2. 跳过次要版本
    有很多整数。你不会用完的。你不需要小数点。您的 API 从 1.0 到 1.1 的任何更改都不应该破坏现有的客户端。所以只使用自然数。如果你喜欢用分离来暗示更大的变化,你可以从 v100 开始,然后做 v200 等等,但即便如此,我认为YAGNI和它是矫枉过正的。

  3. 将版本放在 URI 中的最左侧
    假设您的模型中将有多个资源。它们都需要同步进行版本控制。你不能让人们使用资源 X 的 v1 和资源 Y 的 v2。它会破坏一些东西。如果您尝试支持它将在您添加版本时创建维护噩梦,并且无论如何对开发人员都没有任何附加值。所以, http://api.mydomain.com/v1/Resource/12345, whereResource是资源的类型,12345并被资源 id 替换。

你没有问,但是...

  1. 从 URL 路径中省略动词
    REST 是面向资源的。你的 URL 路径中有诸如“CallFoo”之类的东西,它看起来很像一个动词,而不像一个名词。这是错误的。使用原力,卢克。 使用属于 REST 的动词:GET PUT POST DELETE 等等。如果您想对资源进行验证,请执行GET http://domain/v1/Foo/12345/verification. 如果要更新它,请执行POST /v1/Foo/12345.

  2. 将可选参数作为查询参数或有效负载
    可选参数不应位于 URL 路径中(在第一个问号之前),除非您建议这些可选参数构成独立资源。所以,POST /v1/Foo/12345?action=partialUpdate&param1=123&param2=abc

于 2012-11-24T23:25:34.183 回答
4

Don't do either of those things, because they push the version into the URI structure, and that's going to have downsides for your client applications. It will make it harder for them to upgrade to take advantage of new features in your application.

Instead, you should version your media types, not your URIs. This will give you maximum flexibility and evolutionary ability. For more information, see this answer I gave to another question.

于 2012-05-24T22:04:06.727 回答
3

我喜欢使用配置文件媒体类型参数:

application/json; profile="http://www.myapp.com/schema/entity/v1"

更多信息:

https://www.rfc-editor.org/rfc/rfc6906

http://buzzword.org.uk/2009/draft-inkster-profile-parameter-00.html

于 2014-03-23T09:12:05.650 回答
1

It depends on what you call versions in your API, if you call versions to different representations (xml, json, etc) of the entities then you should use the accept headers or a custom header. That is the way http is designed for working with representations. It is RESTful because if I call the same resource at the same time but requesting different representations, the returned entities will have exactly the same information and property structure but with different format, this kind of versioning is cosmetic.

In the other hand if you understand 'versions' as changes in entity structure, for example adding a field 'age' to the 'user' entity. Then you should approach this from a resource perspective which is in my opinion the RESTful approach. As described by Roy Fielding in his disseration ...a REST resource is a mapping from an identifier to a set of entities... Therefore makes sense that when changing the structure of an entity you need to have a proper resource that points to that version. This kind of versioning is structural.

I made a similar comment in: http://codebetter.com/howarddierking/2012/11/09/versioning-restful-services/

When working with url versioning the version should come later and not earlier in the url:

GET/DELETE/PUT onlinemall.com/grocery-store/customer/v1/{id}
POST onlinemall.com/grocery-store/customer/v1

Another way of doing that in a cleaner way but which could be problematic when implementing:

GET/DELETE/PUT onlinemall.com/grocery-store/customer.v1/{id}
POST onlinemall.com/grocery-store/customer.v1

Doing it this way allows the client to request specifically the resource they want which maps to the entity they need. Without having to mess with headers and custom media types which is really problematic when implementing in a production environment.

Also having the url late in the url allows the clients to have more granularity when choosing specifically the resources they want, even at method level.

But the most important thing from a developer perspective, you don't need to maintain the whole mappings (paths) for every version to all the resources and methods. Which is very valuable when you have lot of sub-resources (embedded resources).

From an implementation perspective having it at the level of resource is really easy to implement, for example if using Jersey/JAX-RS:

@Path("/customer")
public class CustomerResource {
    ...
    @GET
    @Path("/v{version}/{id}")
    public IDto getCustomer(@PathParam("version") String version, @PathParam("id") String id) {
         return locateVersion(version, customerService.findCustomer(id));
    }
    ...
    @POST
    @Path("/v1")
    @Consumes(MediaType.APPLICATION_JSON)
    public IDto insertCustomerV1(CustomerV1Dto customer) {
         return customerService.createCustomer(customer);
    }

    @POST
    @Path("/v2")
    @Consumes(MediaType.APPLICATION_JSON)
    public IDto insertCustomerV2(CustomerV2Dto customer) {
         return customerService.createCustomer(customer);
    }
...
}

IDto is just an interface for returning a polymorphic object, CustomerV1 and CustomerV2 implement that interface.

于 2014-11-06T22:21:19.913 回答
0

Facebook在 url中进行验证。我觉得 url 版本控制在现实世界中也更干净、更容易维护。

.Net 使以这种方式进行版本控制变得非常容易:

[HttpPost]
[Route("{version}/someCall/{id}")]
public HttpResponseMessage someCall(string version, int id))
于 2014-08-20T17:44:07.017 回答