开发者

Client Server API pattern in REST (unreliable network use case)

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开发者_如何学C in particular) handle a situation like that? Are there any articles/whitepapers on REST for this topic?


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.


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:

func product_POST
    // the canonical representation need not contain every field in
    // the request, just those which contribute to its "identity"
    tags = join sorted request.tags
    canonical = join [request.name, request.maker, tags, request.desc]
    id = hash canonical

    if id in products
        http303 products[id]
    else
        products[id] = create_product_from request
        http201 products[id]
    end
end

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.


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 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.

0

上一篇:

下一篇:

精彩评论

暂无评论...
验证码 换一张
取 消

最新问答

问答排行榜