3

I'm having a design issue in my REST api, where I have a resource Device, which happens to represent an IoT device, can be partially updated using PATCH, but some of the things that get updated will begin a long running job.

For instance,

Here is class Device:

public class Device
{
    public int Id { get; set; }
    public string Name { get; set; }
    public string Ip { get; set; }
    public string Foo {get; set; }
}

I can do a PATCH request like so:

PATCH /api/devices/1234
{
    name: "abc123",
    Foo: "hello"
}

which will update both the Name & Bar properties of the device.

However, if I want to update the Ip field using partial update like so:

PATCH /api/devices/1234
{
    name: "abc123",
    ip: "1.2.3.4"
}

The issue is updating the ip on Device triggers an API call to the physical IoT device to set the ip, which causes the device to reboot.

Usually for long running jobs, I would do something similar to what is described here, and return 202 accepted with the URL of the resource so you can track the operation.

Is it a good idea to do this in conjunction with partial update? Say the ip field is specified in my patch, does it make sense to do something like this:

PATCH /api/devices/1234
{
    name: "abc123",
    ip: "1.2.3.4"
}
Response:
{
    location: "/api/tasks/123"
}

This way the request can be tracked, so that the client can know when the device has successfully rebooted... But it feels awkward that the API will only sometimes respond like this. Only if you specify a property that will trigger a long-running job... Not sure what the best design choice is here.

Any advice is welcome.

7
  • 2
    Long-running jobs are a POST, not a PATCH. If you need to reboot, I suggest you perform that operation separately with a POST. Doing it this way will also allow you to PATCH several different values before you POST your reboot. Oct 11, 2017 at 21:10
  • I see, so that being said, having a long-running job doesn't make sense to work in conjunction with a PATCH. In that case the only way to set the ip on my resource will be through a POST hitting a different endpoint.
    – JSarwer
    Oct 11, 2017 at 21:12
  • 1
    Why does it need to be a different endpoint? Oct 11, 2017 at 21:13
  • I suppose it doesn't, but in other areas of the API I have a separate resource for each long running job. I may end up doing that.
    – JSarwer
    Oct 11, 2017 at 21:17
  • Yeah, I missed the part where you said that setting the IP causes a reboot anyway. Seems like PATCH would be suitable for this, provided you return right away and don't block. Returning a location URL to get status in the response seems fine to me. The tricky part might be what to do if someone makes this PATCH request a second time, before the device has successfully rebooted. Oct 11, 2017 at 21:21

2 Answers 2

1

RESTful Casuistry has a pretty good discussion of restart; if you aren't already familiar with it, you should have a look at it.

Usually for long running jobs, I would do something similar to what is described here, and return 202 accepted with the URL of the resource so you can track the operation.

Is it a good idea to do this in conjunction with partial update?

Yes.

But it feels awkward that the API will only sometimes respond like this.

Well, there's nothing necessarily wrong with always reporting that the request has been accepted.

I've come to find that the easiest way to work through this is to think about the messages first, and then worry about the meta data afterwards.

In HTTP, the response to an unsafe operation is generally a message describing the result of the action. If you imagine a website submitting a form, you might get a page back saying "the operation succeeded, follow this link to see the result", or you might get a page back saying "the operation was accepted, follow this link to get the latest status".

The same idea expressed another way: you are navigating an integration protocol; sometimes the application transitions to the finished state, other times it transitions to an intermediate pending state.

The intermediate state really isn't inherently different from a failed state, where you get a response back describing the fact that the request you submitted was rejected, and here's what you can do about it.

The HTTP status code, and the response headers, are not the response; they are meta data lifted from the response. What we're really doing there is taking semantics from the response (which targets the consumer), and lifting some of them into the metadata so that the generic components participating in the protocol have a coarse understanding of what's going on, and can act appropriately.

But it feels awkward that the API will only sometimes respond like this.

Think about the web sites we have today. Sometimes you click on link, and it takes you to the data you expect; but sometimes instead you are directed to a log in screen because your previous license has expired.

In other words, we do this sort of thing all the time; there are multiple states that the application might transition to, the server picks one, and the client has to figure out how to make progress.

The REST answer to the latter is hypermedia, which is to say the client needs to understand the semantics of the links that could be returned by the server, and the server chooses to respond with the appropriate links, including the semantic annotations that distinguish them.

In your example, most of your use cases have data ready immediately, so the "result of the operation" response will include only the link that informs the client of how to view the result

<a ref="http://example.org/viewResult" href="...">

For the restart case, which is going to take some time, the "result of the operation response will include only the link that informs the client of how to check for progress

<a rel="http://example.org/checkProgress" href="...">

Another way of thinking about this: the presence of the link tells the client that a resource is available, the link relation tells the client what the resource is, the protocol tells the client what actions they can take with that kind of resource (which http methods are supported, what media-types are available, and so on).

Atom Publishing is a really good example of a protocol defined this way.

2

Create a GUID immediately and return it in the response after asynchronously initiating the job:

{
    "UpdateId": myguid,
    "Result": "Update Started"
}

You then have an additional method:

DeviceUpdateStatus

This takes the guid as an argument and can be queries so that the client knows whether the update has completed or not.

4
  • This is one way to deal with long-running jobs, but my main issue is the fact that I would be creating a long-running job from a PATCH request, which would only create one if I am updating a specific property.
    – JSarwer
    Oct 11, 2017 at 21:21
  • @JSarwer See the second property, Result? You can set it to "Update Complete" for when a long running job is not needed Oct 11, 2017 at 21:30
  • 1
    @JSarwer you can use polymorphism to provide a compiler checked response to the client that is either the long running task started response or a different response when the PATCH doesn't need to start a task. You may want the responses to look more different than just a different Result value, and polymorphism is a good solution.
    – Samuel
    Oct 11, 2017 at 21:58
  • @Samuel good point, no disagreement from me there. Oct 12, 2017 at 14:25

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

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