70

I am building an API where the user can ask the server to perform multiple actions in one HTTP request. The result is returned as a JSON array, with one entry per action.

Each of these actions might fail or succeed independently of each other. For instance, the first action might succeed, the input to the second action might be poorly formatted and fail to validate and the third action might cause an unexpected error.

If there was one request per action, I would return status codes 200, 422 and 500 respectively. But now when there is only one request, what status code should I return?

Some options:

  • Always return 200, and give more detailed information in the body.
  • Maybe follow the above rule only when there is more than one action in the request?
  • Maybe return 200 if all requests succeed, otherwise 500 (or some other code)?
  • Just use one request per action, and accept the extra overhead.
  • Something completely different?
  • 3
    Your question made me think about an other one: programmers.stackexchange.com/questions/309147/… – AilurusFulgens Aug 29 '16 at 13:28
  • 7
    Slightly related as well: programmers.stackexchange.com/questions/305250/… (see the accepted answer about the separation between HTTP status codes and application codes) – Tibo Aug 29 '16 at 13:39
  • 4
    What is the advantage you achieve by grouping those requests together? Is it about business logic, like a transaction over multiple resources, or is it about performance? Or something else? – Luc Franken Aug 29 '16 at 13:54
  • 5
    Ok, in that case I would strongly suggest improving that performance on other areas. Try things like optimistic ui, request batching, caching etc. before implementing this complexity into your business layer. Do you have clear insight in where you lose most time? – Luc Franken Aug 29 '16 at 14:37
  • 4
    ... don't be too hopeful that people will correctly look at those statuses. Most programs only check for the most common ones and fail or misbehave if they get an unexpected status code. (I remember there was also a presentation at DefCon about protecting your site from crawlers by sending random exit statuses which the browser ignores and simply displays why crawlers sometimes take to be errors and thus stop crawling that part of your website). – Bakuriu Aug 29 '16 at 19:10
21

The short, direct answer

Since the request speaks of executing the list of tasks (tasks are the resource that we're speaking of here), then if the task group has been moved forward to execution (that is, regardless of execution result), then it would be sensible that the response status will be 200 OK. Otherwise, if there was a problem that would prevent execution of the task group, such as failing validation of the task objects, or some required service isn't available for example, then the response status should denote that error. Past that, when execution of the tasks commences, seeing as the tasks to perform are listed in the request body, then I would expect that the execution results will be listed in the response body.


The long, philosophical answer

You are experiencing this dilemma because you are diverting from what HTTP was designed for. You are not interacting it to manage resources, rather, you are using it as means of remote method invocation (which is not very odd, however works poorly without a preconceived scheme).

With the above being said, and without courage to turn this answer into a long opinionated guide, the following is a URI scheme that conforms with a resource management approach:

  • /tasks
    • GET lists all tasks, paginated
    • POST adds a single task
  • /tasks/task/[id]
    • GET responds with a single task's state object
    • DELETE cancels/deletes a task
  • /tasks/groups
    • GET lists all task groups, paginated
    • POST adds a group of tasks
  • /tasks/groups/group/[id]
    • GET responds with a task group's state
    • DELETE cancels/deletes the task group

This structure talks about resources, not what to do with them. What is being done with resources is the concern of another service.

Another important point to make is that it's advisable to not block for very long in an HTTP request handler. Much like UI, an HTTP interface should be responsive -- in a timescale that is a few orders of magnitude slower (because this layer deals with IO).

Making the move toward designing an HTTP interface that strictly manages resources is likely as hard as moving work away from a UI thread when a button is clicked. It requires that the HTTP server communicates with other services to execute tasks rather than executing them in the request handler. This isn't a shallow implementation, it's a change in direction.


Some examples of how such a URI scheme would be used

Executing a single task and tracking progress:

  • POST /tasks with the task to execute
    • GET /tasks/task/[id] until response object completed has positive value while showing current status/progress

Executing a single task and awaiting its completion:

  • POST /tasks with the task to execute
    • GET /tasks/task/[id]?awaitCompletion=true until completed has positive value (likely has timeout, which is why this should be looped)

Executing a task group and tracking progress:

  • POST /tasks/groups with the group of tasks to execute
    • GET /tasks/groups/group/[groupId] until response object completed property has value, showing individual task status (3 tasks completed out of 5, for example)

Requesting an execution for a task group and waiting for its completion:

  • POST /tasks/groups with the group of tasks to execute
    • GET /tasks/groups/group/[groupId]?awaitCompletion=true until responds with result that denotes completion (likely has timeout, which is why should be looped)
  • I think talking about what makes sense semantically is the right way to approach this. Thanks! – Anders Aug 30 '16 at 10:31
  • 2
    I was going to propose this answer if it hadn't been there already. It is impossible to make multiple requests in a single HTTP request. On the other hand, it is perfectly possible to make a single HTTP request which says "perform the following actions, and let me know what the results are". And this is what is happening here. – Martin Kochanski Aug 30 '16 at 12:04
  • I will accept this answer even thoug it has far from the most votes. While other answers are good too, I think this is the only one that reasons about the semantics of HTTP. – Anders Aug 31 '16 at 20:00
86

My vote would be to split these tasks into separate requests. However if too many round trips are a concern, I did come across HTTP response code 207 - Multi-Status

Copy/paste from this link:

A Multi-Status response conveys information about multiple resources in situations where multiple status codes might be appropriate. The default Multi-Status response body is a text/xml or application/xml HTTP entity with a 'multistatus' root element. Further elements contain 200, 300, 400, and 500 series status codes generated during the method invocation. 100 series status codes SHOULD NOT be recorded in a 'response' XML element.

Although '207' is used as the overall response status code, the recipient needs to consult the contents of the multistatus response body for further information about the success or failure of the method execution. The response MAY be used in success, partial success and also in failure situations.

  • 22
    207 does seem be what the OP wants, but I really want to stress that its probably a bad idea to have this multi-request-in-one approach. If the concern is performance then you should be architecting for cloud-style horizontally scalable systems (which is something that HTTP-based systems are great at) – David Grinberg Aug 29 '16 at 14:21
  • 43
    @DavidGrinberg I couldn't disagree more. If the individual actions are cheap, then the overhead of handling a request can be much more costly than the action itself. Your suggestion could lead to scenarios where updating multiple rows in a database is done using a separate transaction per row because each row is sent as a separate request. This is not only horribly inefficient, but also means it won't be possible to update multiple rows atomically if that is needed. Horizontal scaling is important but it is not a replacement for efficient designs. – kasperd Aug 29 '16 at 19:02
  • 4
    Well said and pointing out a typical issue of REST API implementations done by people ignorant towards the realities of business needs such as performance and/or atomicity. Which is why, for example, the OData REST specification has a batch format for multi operations in one call - there is a real need for it. – TomTom Aug 29 '16 at 19:21
  • 8
    @TomTom, the OP doesn't want atomicity. That would be an much easier thing to design, as there is only one status of an atomic operation. Also, the HTTP spec does allow batching operations for performance, via HTTP/2 multiplexing (naturally, HTTP/2 support is another matter, but the spec allows for it). – Paul Draper Aug 29 '16 at 20:29
  • 2
    @David Having worked on some HPC problems in the past, in my experience the cost of sending a single byte is pretty much the same as sending a thousand (different transfer mediums have different overhead certainly, but it's rarely better than this). So if performance is a concern I don't see how sending multiple requests wouldn't have a large overhead. Now if you could multiplex multiple requests over the same connection this issue would disappear, but as I understand it, that's only an option with HTTP/2 and support for it is rather limited. Or am I missing something? – Voo Aug 29 '16 at 20:51
24

Allthough multi-status is an option, I would return 200 (All is well) if all requests succeeded and an error (500 or maybe 207) otherwise.

The standard case should usually be 200 - everything works. And clients should only have to check for that. And only if the error-case happened you can return a 500 (or a 207). I think the 207 is a valid choice in the case of at least one error, but if you see the whole package as one transaction you could also send 500. - The client will want to interpret the error-message either way.

Why not always send 207 ? - Because standard cases should be easy and standard. While exceptional cases can be exceptional. A client should only have to read the response body and do further complex decisions, if an exceptional situation warrants it.

  • 6
    I don't quite agree. If subrequest 1 and 3 succeeded, you get a combined resource and have to check the combined response anyway. You just have one more case to consider. If response = 200 or subresponse 1 = 200 then request 1 succeeded. If response = 200 or subresponse 2 = 200 then request 2 succeed and so on instead of just testing the sub response. – gnasher729 Aug 29 '16 at 14:38
  • 1
    @gnasher729 it really depends on the application. I imagine a user-driven action, which will simply flow to the next step with (everything ok) when all requests succeeded. - If anything went wrong (global state <= 200) then you have to display detailed errors and change the workflow, and only need a single check for each subrequest, since you are in the "handleMixedState" function and not the "handleAllOk" function. – Falco Aug 29 '16 at 15:36
  • It really does depend on what it means. For example I have an endpoint that controls trading strategies. You can "start" a list of identifiers in one run. Return 200 means the operation (process them) succeeded - but not all may start successfully. Which, btw., can not even be seen in the immediate result (which will be starting) as the startup can take some seconds. The semantics in multi operation calls depend on the scenario. – TomTom Aug 29 '16 at 19:22
  • I would also most likely send a 500 if there was a general Problem (e.g. Database down) so the server doesn't even try individual requests, but can just return a general error. - Because there are 3 different results for the user 1. all OK, 2. general Problem, nothing works, 3. Some requests failed. -> Which will usually lead to a completely different program-flow. – Falco Aug 30 '16 at 8:48
  • 1
    Ok, so one approach would be: 207 = individual status for each request. Anything else: Status returned applies to each request. Makes sense for 200, 401, ≥ 500. – gnasher729 Aug 30 '16 at 12:42
13

One option would be to always return a status code 200 and then return specific errors in your JSON document body. This is exactly how some APIs are designed (they always return a status code 200 and dispatch the error in the body). For more details about the different approaches, see http://archive.oreilly.com/pub/post/restful_error_handling.html

  • 2
    In this case, I like the idea of using the 200 to indicate all is well, request is received and was valid, and then use the JSON to provide details on what happened in that request (i.e. the result of the transactions). – rickcnagy Aug 30 '16 at 3:58
4

I think neilsimp1 is correct, but I would recommend a redesign of the data being sent in such a way that you could send a 206 - Accepted and process the data later. Perhaps with callbacks.

The problem with trying to send multiple actions in a single request is exactly the fact that each action should have it's own "status"

Looking at importing a CSV (I know not really what the OP is about but it is a simple version). POST the CSV and get back a 206. Then later the CSV can be imported and you can get the status of the import with a GET (200) against a URL that shows per row errors.

POST /imports/ -> 206
GET  /imports/1 -> 200
GET  /imports/1/errors -> 200 -> Has a list of errors

This same pattern can be applied to many batch opterations

POST /operations/ -> 206
GET  /operations/1 -> 200
GET  /operations/1/errors -> 200 - > Has a list of errors.

The code that handles the POST need only verify that the format of the operations data is valid. Then at some later time the operations may be executed. In a back ground worker, so you can scale easier, for example. Then you can check the status of the operations when ever you want. You can use polling or call backs, or streams or whatever to address the need to know when a set of operations complete.

2

Already many good answers here, but one aspect is missing:

What's the contract that your clients expect?

HTTP return codes should indicate at least a success / failure distinction and thus play the role of "poor man's exceptions". Then 200 means "contract completely fulfilled", and 4xx or 5xx indicate failure to fulfill.

Naively, I'd expect the contract of your multiple-actions request to be "do all my tasks", and if one of them fails, then the request wasn't (completely) successful. Typically, as a client I'd understand 200 as "everything ok", and codes from the 400 and 500 family force me to think about the consequences of a (partial) failure. So, use 200 for "all tasks done" and 500 plus a descriptive response in case of a partial failure.

A different hypothetical contract might be "try doing all of the actions". Then it's completely in line with the contract if (some of) the actions fail. So you'd always return 200 plus a results document where you find the success / failure information for the individual tasks.

So, what's the contract you want to follow? Both are valid, but the first one (200 only in case everything was done) is more intuitive to me, and better in line with typical software patterns. And for the (hopefully) majority of cases where the service completed all tasks, it's straightforward for the client to detect that case.

A final important aspect: How do you communicate your contract decision to your clients? E.g. in Java, I'd use method names like "doAll()" or "tryToDoAll()". In HTTP, you can name the endpoint URLs accordingly, hoping that your client developers see, read, and understand the naming (I wouldn't bet on that). One more reason to choose the contract of least surprise.

0

Answer:

Just use one request per action, and accept the extra overhead.

A status code describes the status of one operation. Therefore, it makes sense to have one operation per request.

Multiple independent operations breaks the principal that the request-response model and status codes are based on. You're fighting nature.

HTTP/1.1 and HTTP/2 have made overhead of HTTP requests far lower. I estimate there are very few situations where batching independent requests is advisable.


That said,

(1) You can make multiple modifications with a PATCH request (RFC 5789). However, this requires that the changes for not independent; they are applied atomically (all or nothing).

(2) Others have pointed out the 207 Multi-Status code. However, this is defined only for WebDAV (RFC 4918), an extension of HTTP.

The 207 (Multi-Status) status code provides status for multiple independent operations (see Section 13 for more information).

...

A Multi-Status response conveys information about multiple resources in situations where multiple status codes might be appropriate. The 'multistatus' root [XML] element holds zero or more 'response' elements in any order, each with information about an individual resource.

An 207 WebDAV XML response would be as odd as a duck in a a non-WebDAV API. Don't do this.

  • 1
    You're essentially asserting that @Anders has an XY problem. You might be right, but unfortunately, that means that you haven't actually answered the question he asked (what status code to use for multi-action request). – Azuaron Aug 29 '16 at 19:34
  • 2
    @Azuaron, what kind of belt works best for beating children? I think "N/A" is an allowable answer. Besides, Andres included multiple requests in his list of ideas. I wholeheartedly supported that option. – Paul Draper Aug 29 '16 at 20:22
  • I somehow missed that he listed that. In that case, I assert it's a silly question, Your Honor! – Azuaron Aug 29 '16 at 20:26
  • 1
    @Azuaron I absolutely think this is a valid answer. If I am doing it all wrong I want someone to say so, and not give me instructions on how to best drive off a cliff. – Anders Aug 30 '16 at 5:54
  • 1
    Nothing forbids to send JSON in the 207 response, as long as the Content-Type header is properly set and matches what the client asked (Accept header). – dolmen Aug 30 '16 at 12:54
0

If you really need to have multiple actions in one request, why not wrap all of the actions in a transaction in the backend? That way they either all succeed or all fail.

As a client using the API, I can deal with complete success or failure on an API call. Partial success is hard to deal with, since I would have to handle all of the possible resulting states.

  • 2
    I assume if the request should be atomic he wouldn't have posted this question. – Andy Aug 30 '16 at 0:14
  • @Andy Maybe, but you can't assume he's considered all of the implications of such a design. – Dean Aug 30 '16 at 0:52
  • The request should not be atomic - e.g. if #2 fails, the changes made by #1 should still persist. So wrapping everything in one transaction is not an option. – Anders Aug 30 '16 at 10:34

protected by gnat Aug 30 '16 at 12:59

Thank you for your interest in this question. Because it has attracted low-quality or spam answers that had to be removed, posting an answer now requires 10 reputation on this site (the association bonus does not count).

Would you like to answer one of these unanswered questions instead?

Not the answer you're looking for? Browse other questions tagged or ask your own question.