RESTful undelete

我的梦境 提交于 2019-12-02 16:31:45

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.

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.

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.

Nurbol Alpysbayev

"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 }

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.

Now to undelete, the correct way is to PATCH. Contrary to a PUT which is used to update an item, the PATCH is expected to be an operation on an 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.

We've forced the model creating a

POST /modelname/:id/undelete

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!