RESTful undelete
Asked Answered
M

7

75

It is a fairly common requirement to support undeletes or delayed/batched deletions for data services. What I'm wondering is how to implement this in a RESTful way. I'm torn between a few different options (none of which seems terribly attractive to me). Common across these different options, I believe, is the need for an API which returns all resource marked as deleted for a particular resource type.

Here are some options I've thought about and some of their pros/cons:

Options to mark resource as deleted:

  • Use HTTP DELETE to mark the resource as deleted.
  • Use HTTP PUT/POST to update deleted flag. This doesn't feel right since it maps what is essentially a deletion away from the HTTP DELETE method and into other HTTP methods.

Options when GET-ing resource marked for deletion:

  • Return HTTP Status 404 for a resource marked as deleted. Clean & transparent, but how do we tell the difference between a resource that is really deleted vs. one just marked as deleted.
  • Return HTTP Status 410. Provides way to tell the difference, but 410 technically says it "is expected to be considered permanent. Clients with link editing capabilities SHOULD delete references to the Request-URI after user approval." There may be enough wiggle room in the words "expected" and "SHOULD" here. Not sure how well 410 is supported/understood out there in clients.
  • Return HTTP Status 200 and include flag field indicating resource is deleted. This seems wierd since the idea of deleting it in the first place was because you actually wanted it to not appear. This pushes responsibility for filtering out deleted resources down to clients.

Options for responses which include this deleted resource:

  • Omit the resources makred as deleted. Clean & simple. But what if you actually want to know about deleted resources.
  • Include them along with field indicating that they are deleted. This pushes responsibility for filtering out deleted resources down to clients. It makes pagination tricky if you want to only page through active or deleted resources.

Options when updating a resource marked for deletion:

  • Use HTTP Status 404. The resource is gone right? But, how can you tell the difference between a resource marked as deleted and one actually deleted. HTTP body in 404 response could disambiguate here but then clients are left with parsing/interpreting your body to disambiguate. Maybe response header might help here? Which one? Custom header?
  • Use HTTP Status 409 with message about how resource must first be undeleted.

Options to undelete resource marked for deletion:

  • Use HTTP PUT/POST for update operation of resource and mark it as active again. This only works as long as you're not returning an HTTP 404 for the GET operation for the resource since it doesn't make since to PUT/POST to a resource that is "Not found" (404).
  • Use HTTP PUT/POST for creation operation of resource. The problem here is which data takes precedence? The data sent up in the create operation? Or the data that is being undeleted? filter it out of any other queries that would have returned it. Then, treat the HTTP PUT/POST that creates the resource as an undelete if the resource identifier points to a resource marked as deleted.
  • Separate REST path dedicated to undelete resources marked for deletion.

This is by no means an exhaustive list. I just wanted to enumerate some of the options that have been bouncing around in my head.

I know the answer to how to do this is, as usual, "it depends". What I'm curious about is what qualifications/requirements would you use to make your decision? How have you seen this implemented or implemented it yourself?

Monosyllabic answered 23/12, 2011 at 23:56 Comment(1)
This is by far one of the most well-prepared and well-thought-out questions I have ever seen on StackOverflow. Have an upvote.Husch
F
11

Going by the book: RFC 2616-9.7:

  The DELETE method requests that the origin server delete the resource 
  identified by the Request-URI. This method MAY be overridden by human 
  intervention (or other means) on the origin server. The client cannot
  be guaranteed that the operation has been carried out, even if the 
  status code returned from the origin server indicates that the action
  has  been completed successfully. However, the server SHOULD NOT 
  indicate success unless, at the time the response is given, if it intends
  to delete the resource or move it to an inaccessible location.

When you DELETE a resource the server should mark the resource for deletion on it's side. It doesn't really have to delete the resource, it just can't give any guarantee that the operation has been carried out. Even so, the server shouldn't say it's been deleted when it hasn't.

  A successful response SHOULD be 200 (OK) if the response includes an entity
  describing the status, 202 (Accepted) if the action has not yet been enacted,
  or 204 (No Content) if the action has been enacted but the response does not
  include an entity.

If the operation is delayed, send a 202 and an entity body describing the result of the action. (Think of a poll-able "task" representing the server's deferred deletion of the resource; It could theoretically leave it forever in that state.) All it has to do is prevent the client from retrieving it again in it's original form. Use a 410 for the response code, and when the "task" finishes or the server otherwise deletes the resource, return a 404.

However, if a DELETE's semantics don't make sense for the resource in question, perhaps it's not a deletion you're looking for, but an addition state transition that alters the resource state but keeps it accessible? In that case, use a PUT/PATCH to update the resource and be done.

Foliose answered 24/12, 2011 at 2:10 Comment(3)
Any suggestions on how one could RESTfully undelete a resource after it's been deleted?Monosyllabic
In that case, you aren't actually deleting the resource, you're just restricting access to it. Theoretically though, there wouldn't be anything stopping you from, having DELETE'd the resource, adding a state transition from that task to a "recreate" it. I don't really like that approach from a semantic purity perspective, but at least it's cleaner than bypassing HTTP semantics.Foliose
Good answer, but since 2616 has since been superseded by 7231 which has a different description of DELETE semantics, and I had some other things to add anyway, I've posted a new one.Reames
R
9

The Short Version

You cannot RESTfully undelete a resource using any method on it's original URI - it's illogical, because any operation attempted on a resource that has been deleted should return either a 404 or a 410. While this is not explicitly stated in the spec, it's strongly implied in the definition of the DELETE method 1 (emphasis added):

In effect, this method is similar to the rm command in UNIX: it expresses a deletion operation on the URI mapping of the origin server rather than an expectation that the previously associated information be deleted.

In other words, when you've DELETEd a resource, the server no longer maps that URI to that data. So you can't PUT or POST to it to make an update like "mark this as undeleted" etc. (Remember that a resource is defined as a mapping between a URI and some underlying data).

Some Solutions

Since it's explicitly stated that the underlying data is not necessarily deleted, it doesn't preclude the server making a new URI mapping as part of the DELETE implementation, thereby effectively making a backup copy that can be restored later.

You could have a "/deleted/" collection that contains all the deleted items - but how would you actually perform the undelete? Perhaps simplest RESTful way is to have the client retrieve the item with GET, and then POST it to the desired URL.

What if you need to be able to restore the deleted item to it's original location? If you're using a media type that supports it, you could include the original URI in the response to a GET from the /deleted/ collection. The client could then use it to POST. Such a response might look like this in JSON:

{
    "original-url":"/some/place/this/was/deleted/from",
    "body":<base64 encoded body>
}

The client could then POST that body to that URI to perform an undelete.

Alternatively, if your resource definition allows the concept of moving (by updating a "location" property or something like that) then you can do a partial update and avoid the round trip of the entire object. Or, do what most people do and implement an RPC-like operation to tell the server to move the resource! UnRESTful, yes but it will probably work fine in most situations.

How You Decide These Things

Regarding the question of how you decide these things: you have to consider what delete means in the context of your application, and why you want it. In a lot of applications, nothing ever gets deleted, and "delete" really just means "exclude this item from all further queries/listings etc. unless I explicitly undelete it". So, it's really just a piece of metadata, or a move operation. In that case, why bother with HTTP DELETE? One reason might be if you want a 2-tiered delete - a soft or temporary version that's undoable, and a hard/permanent version that's, well...not.

Absent any specific application context, I'd be inclined to implement them like this:

I don't want to see this resource any longer, for my convenience: POST a partial update to mark the resource as "temporarily deleted"

I don't want anyone to be able to reach this resource any longer because it's embarrassing/incriminating/costs me money/etc: HTTP DELETE

The next question to consider is: should the permanent delete only unmap the URI permanently, so that no one can link to it any longer, or is it necessary to purge the underlying data too? Obviously if you keep the data, then an administrator could restore even a "permanently" deleted resource (not through any RESTful interface however). The downside of this is that if the owner of the data really wants it purged, then an admin has to do that outside the REST interface.

Reames answered 27/9, 2017 at 21:38 Comment(2)
If a resource returns a 404 on GET, there is no difference from a clien't perspective if it was deleted, or if it never existed in the first place. re-creating a resource with PUT is the same as creating the resource in the first place with PUT. There's nothing wrong with this.Tuggle
@Tuggle You're right. I stated in my answer that you couldn't PUT or POST to a deleted URL, and then went on to suggest a solution that does exactly that! What I meant to say was that you couldn't PUT or POST to update a deleted resource, in other words to remove a "deleted" flag or something along those lines, as suggested in the question. I updated my answer to try to reflect that.Reames
H
7

I think the most RESTful way to solve this is to use HTTP PUT to mark the resource for deletion (and undelete) and then use HTTP DELETE to permanently delete the resource. To get a list of resources marked for deletion I would use a parameter in the HTTP GET request eg. ?state=markedForDeletion. If you requests a resource marked for deletion without the parameter, I think you should return a "404 Not Found" status.

Hibbard answered 24/12, 2011 at 0:23 Comment(0)
P
7

"Deleted" (trashed) items also may be considered as a resource, right? Then we can access this resource in one of these ways (e.g. for a deleted a user):

PATCH deleted_users/{id}
PATCH trash/users/{id}
PATCH deleted/users/{id}

or some people may think this is more restful way:

PATCH deleted/{id}?type=users

and in payload goes something like this:

{ deleted_at: null }
Preceptory answered 24/1, 2018 at 1:58 Comment(0)
G
5

I'm also running in this problem and I've been looking on the Internet for what feels like the best solution. Since none of the main answers I can find seem correct to me, here is my own research results.

Others are right that the DELETE is the way to go. You could include a flag to determine whether it's immediately a permanent DELETE or a move to the trashcan (and probably only administrators can do an immediate permanent DELETE.)

DELETE /api/1/book/33
DELETE /api/1/book/33?permanent

The backend can then mark the book as deleted. Assuming you have an SQL database, it could be something such as:

UPDATE books SET status = 'deleted' WHERE book_id = 33;

As mentioned by others, once the DELETE is done, a GET of the collection does not return that item. In terms of SQL, this means you must make sure not to return an item with a status of deleted.

SELECT * FROM books WHERE status <> 'deleted';

Also, when you do a GET /api/1/book/33, you must return a 404 or 410. One problem with 410 is that it means Gone Forever (at least that's my understanding of that error code,) so I would return 404 as long as the item exists but is marked as 'deleted' and 410 once it was permanently removed.

Note: if you want to keep the data for X amount of time and then delete it permanently in a CRON job, then use a "deleted_on" field with the data the non-permanent delete happened. Then the SQL commabd above changes to something like ... WHERE deleted_on IS NULL.

Now to undelete, the correct way is to PATCH. Contrary to a PUT which is used to update an existing item, the PATCH is expected to be an operation on an existing item. From what I can see, the operation is expected to be in the payload. For that to work, the resource needs to be accessible in some way. As someone else suggested, you can provide a trashcan area where the book would appear once deleted. Something like this would work to list books that were put in the trashcan:

GET /api/1/trashcan/books

[{"path":"/api/1/trashcan/books/33"}]

So, the resulting list would now include book number 33, which you can then PATCH with an operation such as:

PATCH /api/1/trashcan/books/33

{
    "operation": "undelete"
}

If you'd like to make the operation more versatile, you could use something such as:

PATCH /api/1/trashcan/books/33

{
    "operation": "move",
    "new-path": "/api/1/books/33"
}

Then the "move" could be used for other changes of URL wherever possible in your interface. (I am working on a CMS where the path to a page is in one table called tree, and each page is in another table called page and has an identifier. I can change the path of a page by moving it between paths in my tree table! This is where a PATCH is very useful.)

Unfortunately, the RFCs do not clearly define the PATCH, only that it is to be used with an operation as shown above, opposed to a PUT which accepts a payload representing a new version, possibly partial, of the targeted item:

PUT /api/1/books/33

{
    "title": "New Title Here"
}

Whereas the corresponding PATCH (if you were to support both) would be:

PATCH /api/1/books/33

{
    "operation": "replace",
    "field": "title",
    "value": "New Title Here"
}

I think that supporting that many PATCH operations would be crazy. But I think that a few good examples give a better idea of why PATCH is the correct solution.

You can think of it as: using patch is to change a virtual field or run a complex operation such as a move which would otherwise require a GET, POST, DELETE (and that's assuming the DELETE is immediate and you could get errors and end up with a partial move...) In a way, the PATCH is similar to having any number of methods. An UNDELETE or MOVE method would work in a similar way, but the RFC clearly says there is a set of standardized methods and you should certainly stick to them and the PATCH gives you plenty of room to not have to add your own methods. Although I did not see anything in the specs saying you should not add your own methods. If you do, though, make sure to clearly document them.

Gillum answered 3/12, 2018 at 17:32 Comment(1)
I think this is a strong case for using PATCH 👍Sacker
S
1

We've forced the model creating a

POST /modelname/:id/undelete

Stortz answered 4/6, 2019 at 15:30 Comment(3)
this is not restful as you have a verb in the path: undeleteSoever
I confirm. I said "We've forced the model" ... there is not an UNDELETE http verb and so we choose this compromiseStortz
I am not talking about http verb, I am talking about the verb in your path: undelete .Soever
A
0

Another way that would only slightly violate REST principles would be to perform a soft delete when calling DELETE Method of a resources and then do a permanent delete after a certain time. I implemented this approach and it works quite well for applications where you just want to offer the user the possibility to quickly undo his operation.

My concrete implementation was that I just had an additional column called "deletionTimestamp" that was set if a resource is deleted. Then I had a CRON job running every 24 hours that checked for the deletion time of resources and if the deletion time was to far back, the row would get permenantly deleted.

For me the problem with actually deleting and saving a copy of the item was the large overhead of implementing this. I had resources with very complex foreign key relations that would require specific implementations for different types of deleted resources.

Amok answered 8/2, 2023 at 14:14 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.