REST 中的客户端服务器 API 模式(不可靠的网络用例)
假设我们在不可靠的网络上发生客户端/服务器交互(数据包丢失)。客户端正在调用服务器的 RESTful api(通过 http over tcp):
- 向 http://server.com/products
- 服务器正在创建“产品”资源的对象(将其保存到数据库等)
- 服务器正在返回 201 Created with a Location header of "http://server.com/products/12345"
- !包含 http 响应的 TCP 数据包被丢弃,最终导致 tcp 连接重置
我看到以下问题:客户端永远不会获得新创建的资源的 ID,但服务器将创建一个资源。
问题:这是应用程序级别的行为还是框架应该处理这个问题? Web 框架(尤其是 Rails)应该如何处理这样的情况?有关于此主题的 REST 文章/白皮书吗?
Let's assume we have a client/server interaction happening over unreliable network (packet drop). A client is calling server's RESTful api (over http over tcp):
- issuing a POST to http://server.com/products
- server is creating an object of "product" resource (persists it to a database, etc)
- server is returning 201 Created with a Location header of "http://server.com/products/12345"
- ! TCP packet containing an http response gets dropped and eventually this leads to a tcp connection reset
I see the following problem: the client will never get an ID of a newly created resource yet the server will have a resource created.
Questions: Is this application level behavior or should framework take care of that? How should a web framework (and Rails in particular) handle a situation like that? Are there any articles/whitepapers on REST for this topic?
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(5)
当服务器不响应 POST 时,客户端将收到错误。然后,客户端通常会重新发出请求,因为他们认为请求已失败。我突然想到解决这个问题的两种方法。
一是客户端可以生成某种请求标识符,例如 guid,并将其包含在请求中。如果服务器收到具有重复 GUID 的 POST 请求,则它可以拒绝该请求。
另一种方法是使用 PUT 而不是 POST 来创建。如果无法让客户端生成 URI,那么您可以要求服务器使用 GET 提供新的 URI,然后对该 URI 执行 PUT。
如果您搜索“make POST idempot”之类的内容,您可能会找到一堆关于如何执行此操作的其他建议。
The client will receive an error when the server does not respond to the POST. The client would then normally re-issue the request as they assume that it has failed. Off the top of my head I can think of two approaches to this problem.
One is that the client can generate some kind of request identifier, such as a guid, which it includes in the request. If the server receives a POST request with a duplicate GUID then it can refuse it.
The other approach is to PUT instead of POST to create. If you cannot get the client to generate the URI then you can ask the server to provide a new URI with a GET and then do a PUT to that URI.
If you search for something like "make POST idempotent" you will probably find a bunch of other suggestions on how to do this.
如果创建重复的资源(例如具有相同标题、描述等的产品)是不合理的,则可以在服务器上生成唯一标识符,可以针对创建的资源进行跟踪,以防止处理重复的请求。与 Darrel 的建议不同在客户端上生成唯一的 ID,这也将防止单独的用户创建重复的资源(您可能会或可能不希望这样做)。客户端将能够通过响应代码(在下面的示例中分别为 201 和 303)来区分“创建”响应和“重复”响应。
用于生成此类标识符的伪代码 — 在本例中,是请求的规范表示的哈希值:
此 ID 可能是也可能不是所创建资源的 URI 的一部分。就我个人而言,如果 URI 将暴露给用户,我倾向于单独跟踪它们(以额外的查找表为代价),因为哈希值往往很难看并且人类难以记住。
在许多情况下,在一段时间后让这些独特的哈希值“过期”也是有意义的。例如,如果您要开发一个转账 API,则用户在几分钟内向同一个人转账相同金额的资金可能表明客户端从未收到“成功”响应。另一方面,如果用户每月向同一个人转账一次相同金额的钱,他们可能正在支付租金。 ;-)
If it isn't reasonable for duplicate resources to be created (e.g. products with identical titles, descriptions, etc.), then unique identifiers can be generated on the server which can be tracked against created resources to prevent duplicate requests from being processed. Unlike Darrel's suggestion of generating unique IDs on the client, this would also prevent separate users from creating duplicate resources (which you may or may not find desirable). Clients will be able to distinguish between "created" responses and "duplicate" responses by their response codes (201 and 303 respectively, in my example below).
Pseudocode for generating such an identifier — in this case, a hash of a canonical representation of the request:
This ID may or may not be part of the created resources' URIs. Personally, I'd be inclined to track them separately — at the cost of an extra lookup table — if the URIs were going to be exposed to users, as hashes tend to be ugly and difficult for humans to remember.
In many cases, it also makes sense to "expire" these unique hashes after some time. For example, if you were to make a money transfer API, a user transferring the same amount of money to the same person a few minutes apart probably indicates that the client never received the "success" response. If a user transfers the same amount of money to the same person once a month, on the other hand, they're probably paying their rent. ;-)
您所描述的问题归结为避免所谓的“双重添加”。正如其他人所提到的,您需要使您的帖子具有幂等性。
这可以在框架级别轻松实现。该框架可以保留已完成响应的缓存。请求必须具有请求唯一,以便任何重试都被视为重试,而不是被视为新请求。
如果成功的响应在发送给客户端的途中丢失,客户端将使用相同的唯一请求重试,然后服务器将使用其缓存的响应进行响应。
您需要考虑缓存的持久性、保留响应的时间等。一种方法是在给定时间段后从服务器缓存中删除响应,这将取决于您的应用程序域和流量,并且可以保留为可配置的踩在框架件上。另一种方法是强制客户端发送确认。确认可以作为单独的请求发送(请注意,这些请求也可能会丢失),也可以作为真实请求上的额外数据发送。
尽管我的建议与其他人的建议类似,但我强烈建议您保留这一层网络弹性,仅执行此操作,处理丢弃请求/响应,并且不允许它处理来自应用程序级别的单独请求的重复资源任务。合并这两部分将混淆所有功能,并且不会让您明确职责分离。
这不是一个简单的问题,但如果你保持它的干净,你可以使你的应用程序更能适应不良网络,而不会引入太多的复杂性。
对于其他人的一些相关经验,请访问 这里。
祝你好运。
The problem as you describe it boils down to avoiding what are called double-adds. As mentioned by others, you need to make your posts idempotent.
This can be easily implemented at the framework level. The framework can keep a cache of completed responses. The requests have to have a request unique so that any retries are treated as such, and not as new requests.
If the successful response gets lost on its way to the client, the client will retry with the same request unique, the server will then respond with its cached response.
You are left with durability of the cache, how long to keep responses, etc. One approach is to remove responses from the server cache after a given period of time, this will depend on your app domain and traffic and can be left as a configurable step on the framework piece. Another approach is to force the client to sent acknowledgements. The acks can be sent either as separate requests (note that these could be lost too), or as extra data piggy backed on real requests.
Although what I suggest is similar to what others suggest, I strongly encourage you to keep this layer of network resiliency to do only that, deal with drop requests/responses and not allow it to deal with duplicate resources from separate requests which is an application level task. Merging both pieces will mush all functionality and will not leave you with a clear separation of responsibilities.
Not an easy problem, but if you keep it clean you can make your app much more resilient to bad networks without introducing too much complexity.
And for some related experiences by others go here.
Good luck.
正如其他响应者所指出的,这里的基本问题是标准 HTTP POST 方法不像其他方法那样具有幂等性。目前正在努力建立幂等 POST 方法的标准,称为 Post-Once-Exactly(POE)。
现在我并不是说这对于您所描述的情况下的每个人来说都是一个完美的解决方案,但如果您同时编写服务器和客户端,您也许可以利用 POE 的一些想法。草案在这里:https://datatracker.ietf.org /doc/html/draft-nottingham-http-poe-00
这不是一个完美的解决方案,这可能就是为什么自草案提交以来的六年里它没有真正起飞的原因。这里讨论了一些问题和一些巧妙的替代选项:
http://tech.groups.yahoo.com/group/rest-讨论/消息/7646
As the other responders have pointed out, the basic problem here is that the standard HTTP POST method is not idempotent like the other methods. There is an effort underway to establish a standard for an idempotent POST method known as Post-Once-Exactly, or POE.
Now I'm not saying that this is a perfect solution for everybody in the situation you describe, but if it is the case that you are writing both the server and the client, you may be able to leverage some of the ideas from POE. The draft is here: https://datatracker.ietf.org/doc/html/draft-nottingham-http-poe-00
It isn't a perfect solution, which is probably why it hasn't really taken off in the six years since the draft was submitted. Some of the problems, and some clever alternate options are discussed here:
http://tech.groups.yahoo.com/group/rest-discuss/message/7646
HTTP 是无状态协议,这意味着服务器无法打开 HTTP 连接。所有连接均由客户端初始化。所以你无法在服务器端解决这样的错误。
我能想到的唯一解决方案:如果您知道哪个客户创建了该产品,您可以向它提供它创建的产品,如果它提取该信息。如果客户不再联系您,您将无法传递有关新产品的信息。
HTTP is a stateless protocol, meaning the server can't open an HTTP connection. All connections get initialized by the client. So you can't solve such an error on the server side.
The only solution I can think of: If you know, which client created the product, you can supply it the products it created, if it pulls that information. If the client never contacts you again, you won't be able to transmit information about the new product.