90

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?
17
  • 3
    Your question made me think about an other one: programmers.stackexchange.com/questions/309147/… Aug 29, 2016 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)
    – user44761
    Aug 29, 2016 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? Aug 29, 2016 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? Aug 29, 2016 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, 2016 at 19:10

8 Answers 8

35

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

I suspect that you are experiencing this dilemma because you are diverting from what HTTP was designed for. I suspect that you are attempting to use it as means of RMI (Remote Method Invocation) rather than as means to manage resources.

The RMI perspective is that you would design your URI scheme as you would functions in an application, and upon request, these would execute an action, and then return its result. Although these types of implementations are relatively common still, these often produce situations HTTP is in the way, rather than making things easy.

(Just to note, RMI through HTTP has its merits in some instances, though you'd still be wise to implement these methods in a non-blocking manner. You could maybe offer startTask and getTaskStatus for instance, both of which would return instantly.)

The design of HTTP is asking you to use it to manage resources instead. It wants to express things like "add a task" (via POST), "get a task" (via GET), "delete a task" (via DELETE) and so on. Designing our URI scheme in that way, we rarely find ourselves in conflict with what HTTP has to offer.

To provide an example of what I mean by a URI scheme that conforms to resource management (vs RMI), here's a layout that might work for your case:

  • /task?complete=[true/false]&start=[start_timestamp]&end=[end_timestamp] ...
    • GET searches for tasks according to querystring
    • POST adds a single task
  • /task/[id]
    • GET responds with a single task's state object
  • /task/[id]/cancellation_request
    • POST adds a cancellation request for the task.
  • /task/[id]/[property_name]
    • GET returns the value of the property of a task of the specified id
  • /task_group?complete=[true/false]&start=[start_timestamp]&end=[end_timestamp] ...
    • GET searches for task groups according to querystring
    • POST adds a group of tasks
  • /task_group/[id]
    • GET responds with a task group object, which includes a list of task objects of all of the tasks in the group.

... and so on

As you may have guessed, task execution in this scheme would be an asynchronous thing -- POST to /task would not wait until the task has completed, or even until it actually started running -- It would simply queue it for execution and then respond that it succeeded to add that task, or that it failed, if the queue is full, for instance.

Note how the URIs have no verbs in them -- They represent resources or collections of resources. The only verbs in this entire scheme are the HTTP methods that are invoked upon these URIs (GET/POST in this case).

Just to hammer this in a little more, URI stands for "Unified Resource Identifier".


Examples of how the above URI scheme would be used

Executing a single task and tracking progress:

  • POST /task with the task to execute
  • GET /task/[id] until response object complete has positive value while showing current status/progress. You can also implement updates with websocket if you want to avoid polling.

Executing a task group and tracking progress:

  • POST /task_group with the group of tasks to execute
  • GET /task_group/[groupId] until response object complete property has positive value, showing individual task status (3 tasks completed out of 5, for example)
3
  • I think talking about what makes sense semantically is the right way to approach this. Thanks!
    – Anders
    Aug 30, 2016 at 10:31
  • 4
    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. Aug 30, 2016 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, 2016 at 20:00
94

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.

12
  • 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) Aug 29, 2016 at 14:21
  • 51
    @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, 2016 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, 2016 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). Aug 29, 2016 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, 2016 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.

5
  • 8
    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, 2016 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, 2016 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, 2016 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, 2016 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, 2016 at 12:42
14

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

1
  • 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, 2016 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.

7
  • 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, 2016 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. Aug 29, 2016 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, 2016 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, 2016 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, 2016 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.

3
  • 3
    I assume if the request should be atomic he wouldn't have posted this question.
    – Andy
    Aug 30, 2016 at 0:14
  • @Andy Maybe, but you can't assume he's considered all of the implications of such a design.
    – Dean
    Aug 30, 2016 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, 2016 at 10:34

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