views:

321

answers:

3

I'm working on a REST API. The key objects ("nouns") are "items", and each item has a unique ID. E.g. to get info on the item with ID foo:

GET http://api.example.com/v1/item/foo

New items can be created, but the client doesn't get to pick the ID. Instead, the client sends some info that represents that item. So to create a new item:

POST http://api.example.com/v1/item/
hello=world&hokey=pokey

With that command, the server checks if we already have an item for the info hello=world&hokey=pokey. So there are two cases here.

Case 1: the item doesn't exist; it's created. This case is easy.

201 Created
Location: http://api.example.com/v1/item/bar

Case 2: the item already exists. Here's where I'm struggling... not sure what's the best redirect code to use.

301 Moved Permanently? 302 Found? 303 See Other? 307 Temporary Redirect? Location: http://api.example.com/v1/item/foo

I've studied the Wikipedia descriptions and RFC 2616, and none of these seem to be perfect. Here are the specific characteristics I'm looking for in this case:

The redirect is permanent, as the ID will never change. So for efficiency, the client can and should make all future requests to the ID endpoint directly. This suggests 301, as the other three are meant to be temporary.

The redirect should use GET, even though this request is POST. This suggests 303, as all others are technically supposed to re-use the POST method. In practice, browsers will use GET for 301 and 302, but this is a REST API, not a website meant to be used by regular users in browsers.

It should be broadly usable and easy to play with. Specifically, 303 is HTTP/1.1 whereas 301 and 302 are HTTP/1.0. I'm not sure how much of an issue this is.

At this point, I'm leaning towards 303 just to be semantically correct (use GET, don't re-POST) and just suck it up on the "temporary" part. But I'm not sure if 302 would be better since in practice it's been the same behavior as 303, but without requiring HTTP/1.1. But if I go down that line, I wonder if 301 is even better for the same reason plus the "permanent" part.

Thoughts appreciated!


Edit: Let me try to better explain the semantics of this "get or create" operation with a more concrete example: URL shortening. This is actually much closer to my app anyway.

For URL shorteners, the most common operation by far is retrieving by ID. E.g. for http://bit.ly/4Agih5, bit.ly receives an ID of 4Agih5 and must redirect the user to its corresponding URL.

bit.ly already has an API, but it's not truly RESTful. For the sake of example, let me make up a more RESTful API. For example, querying the ID might return all sorts of info about it (e.g. analytics):

GET http://api.bit.ly/item/4Agih5

Now if I want to submit a new URL to bit.ly to shorten, I don't know the ID of my URL in advance, so I can't use PUT. I'd use POST instead.

POST http://api.bit.ly/item/
url=http://stackoverflow.com/ (but encoded)

If bit.ly hasn't seen this URL before, it'll create a new ID for it and redirect me via 201 Created to the new ID. But if it has seen that URL, it'll still redirect me without making a change. This way, I can hit that redirect location either way to get the info/metadata on the shortened URL.

Like this example of URL shortening, in my app, collisions don't matter. One URL maps to one ID, and that's it. So it doesn't really matter if the URL has been shortened before or not; either way, it makes sense to point the client to the ID for it, whether that ID needs to be created first or not.

So I probably won't be changing this approach; I'm just asking about the best redirect method for it. Thanks!

+2  A: 

I'd argue for 303. Supposing right now hello=world&hokey=pokey uniquely identifies item foo, but later item foo's hokey value changes to "smokey"? Now those original values are no longer a unique identifier for that resource. I'd argue that a temporary redirect is appropriate.

JacobM
That's a reasonable forward-looking point. Thanks for the confirmation. I'll probably mark this as answer after a while if no one makes a better point. :)
Aseem Kishore
A: 

POST does not support a 'lookup or create' approach. The server cannot tell the client "I would create that, but it already existed. Look here for the existing entry". None of the 2xx codes work because the request is not successful. None of the 3xx codes work, because the intention is not to redirect the POST to a new resource. And 303 is also not appropriate since nothing changed (see 303 spec).

What you could do is provide a form or template to the client to be used with PUT that tells the client how to construct the PUT URI. If the PUT results in a 200 the client knows the resource existed and if 201 is returned that a new resource has been created.

For example:

Template for URI: http://service/items/{key}

PUT http://service/items/456

[data]

201 Created

or

PUT http://service/items/456

[data]

200 Ok

You can also do a 'create but do not replace if exists' using If-None-Match:


PUT http://service/items/456
If-None-Match: *

[data]

412 Precondition failed 

Jan

Jan Algermissen
I surprised that you would not see this as just another "creative" way to use HTTP. I'm assuming you don't have a problem with case 1. For case 2 if you assume that the resource is a "data processing" resource, then I don't see a problem with doing POST that returns 303. Is the problem that the client is using the same resource for both scenarios?
Darrel Miller
PUT is meant to be used when you know the URI of the resource. In this case, the client does not know the final URI of the resource. (Did you miss my mention that the client doesn't know the key, e.g. 456, in advance? The server decides the key.)POST also doesn't require state changes on the server. It means there *might* be state changes on the server.I'll try to dig up places where I've read this.Regardless, I appreciate your comments, but this is also a case to me where the simplicity of POST here by far outweighs the possible slightly better semantics of PUT + If-None-Match.
Aseem Kishore
Here's an article that confirms my point: http://jcalcote.wordpress.com/2008/10/16/put-or-post-the-rest-of-the-story/ "Create = POST if you are sending a command to the server to create a subordinate of the specified resource, using some server-side algorithm." / "Create can be implemented using an HTTP PUT, if (and only if) the payload of the request contains the full content of the exactly specified URL." / "POST operations may or may not result in additional directly accessible resources."
Aseem Kishore
@Aseem, are you saying that the server would not create the new resource because it 'decides' it already exists? I had the impression there was a key involved which the client could know. Sorry if I misunderstood. I still see a problem with the semantic that the client should take the 303 as the item that it intended to put on the server. That seems like out of band knowledge. Or maybe you just spotted an uncovered general semantic.
Jan Algermissen
@Darrel, I ruled out 303 because the way I understand various comments on 303 implies that POST and 303 means: "the POST changed some other resource, look there". This is not specified in the spec though. I think the issue to think about is how the client can actually detect what has changed? How can it learn what happened due to the POST. It is problematic to take the 303 Location to point to the corresponding item, I think. Requires some digging, I guess.
Jan Algermissen
@Aseem, I do not see how the article relates to the question of how the client could understand what really happened. The question here is not how the server can achieve the server side effect; the question is how the client knows what the URL of the item is. With 201 Created that is clear. The other cases in the question do not support that. But it is an interesting question anyway.Maybe take it to rest-discuss?
Jan Algermissen
@Jan, thanks for your comments again. I tried to clarify with a more concrete example in an edit of my original post. If you still don't agree, that's okay, but take my word for it that in my app (like URL shortening), it's quite sensible: it doesn't matter if the item already exists, the client is just asking for that item, and asking to create it if necessary. I'm just asking what the best redirect method is in the case that the item already exists, as "Found"/"See Other"/etc. all seem reasonable approximations for that message but have varying semantics.
Aseem Kishore
+1  A: 

I think one of the reasons that you are struggling with this scenario is because (unless we are missing some key information) the interaction is not very logical.

Let me explain why I think this. The initial premise is that the user is requesting to create something and has provided some key information for the resource they wish to create.

You then state that if that key information refers to an existing object then you wish to return that object. The problem is that the user did not wish to retrieve an existing object they wished to create a new one. If they cannot create the resource because either it already exists or there is a key collision then the user should be informed of that fact.

Choosing to retrieve an existing object when the user has attempted to create a new one seems to be a misleading approach.

Maybe one alternative would be to return a 404 Bad request if the resource already exists and include a link to the existing object in the entity body. The client application could choose to swallow the bad request error and simply follow the link to the existing entity and by doing so hide the issue from the user. That would be the choice of the client application, but at least the server is behaving in a clear manner.


Based on the new example, let me suggest a completely different approach. It may not work in your case, as always the devil is in the details, but maybe it will be helpful.

From the client's perspective it really has no interest in whether the server is creating a new shortened URL or pulling back an existing one. In fact, whether the server needs to generate a new ID or not is an implementation detail that is completely hidden.
Hiding the creation process could be very valuable. Maybe the server can predict in advance that lots of short urls will soon be requested related to a event such as a conference. It could pre-generate these urls in quite periods to balance the load on its servers.

So, based on that assumption, why not just use

GET /ShortUrl?longUrl=http://www.example.org/en/article/something-that-is-crazy-long.html&suggestion=crazyUrl

If the url already existed then you might get back

303 See Other
Location: http://example.org/ShortUrl/3e4tyz

If it previously didn't, you might get

303 See Other
Location: http://example.org/ShortUrl/crazyurl

I realize that this looks like we are breaking the rules of GET by creating something in response to a GET, but I believe in this case there is nothing wrong with it because client did not ask for the shortened URL to be created and really does not care either way. It is idempotent because does not matter how many times you call it.

One interesting question that I don't know the answer to is whether proxies will cache the initial GET and redirect. That might be an interesting property as future requests by other users for the same url may never need to actually get to the origin server, the proxy could handle the request completely.

Darrel Miller
Thanks for the feedback. Let me try to address your point via another example -- URL shortening -- that's much closer to my actual app. I'll edit my original question to do this.
Aseem Kishore
This is a very interesting point! You're right that my first thought was "this breaks the rules of REST because GET is changing the state on the server", but I do see the value in it -- the client isn't necessarily asking for a change in the state of the server, and it doesn't need to know that the state changed at all. Let me think about this more and raise this idea to my team members. Thanks!
Aseem Kishore
Just wanted to let you know that the team loved your idea, and we've decided to go with it. "Creating new IDs" will be transparent and will be implicitly done via just a GET. Thanks for the suggestion!
Aseem Kishore
Cool, glad to help. I suddenly have a new appreciation for mythical ocean creatures.
Darrel Miller