Search code examples
resthttpasp.net-core-webapietagoptimistic-locking

version number vs ETag for optimistic concurrency


I'm implementing my first Web API with JSON responses and got confused handling concurrency control for optimistic locking. Right now I develop the server with ASP.NET Core 6 and the client is developed with Angular. The data store is a SQL database. In the future we want to open the API to third parties.

I see two options for handling optimistic locking:

A) ETag in header

B) version number in the body

With option A) I see those two problems:

  1. Since ETag is a header "Precondition Fails 412" needs to be send for failures. The error needs to be specified in the body, so the client can understand that it is a concurrency error.

For concurrency errors I would expect to send HTTP error code 409:

"...Conflicts are most likely to occur in response to a PUT request. For example, if versioning were being used and the entity being PUT included changes to a resource which conflict with those made by an earlier (third-party) request, the server might use the 409 response to indicate that it can't complete the request. In this case, the response entity would likely contain a list of the differences between the two versions in a format defined by the response Content-Type." 2) When a collection/list of representations (e.g. all due orders) is returned from the server there is no possibility to send multiple ETags. The ETag in the header applies to the whole collection/list, but each single resource in the list can be individually modified by different users and needs to have a version number for concurrency detection. I don't see any other option than to send an individual version number/ETag as a property with each representation in the body.

I find it confusing when collections are treated differently than single resources. Since the client needs to store the ETags for each single resource it is natural to include the ETag as a property in its object model anyway.

Option B) avoids the issues of A): I'm free to send a 409 on failure and single resources and collections of resources are treated the same way. The problem is the DELETE verb.

There is a discussion about using a body in a DELETE request at [1]: Is an entity body allowed for an HTTP DELETE request?

The current version of the RFC states:

"A client SHOULD NOT generate a body in a DELETE request. A payload received in a DELETE request has no defined semantics, cannot alter the meaning or target of the request, and might lead some implementations to reject the request."

What does "has no defined semantics" mean? What are the implications? I'm aware that some implementations are ignoring the body or have not implemented the option to send a body with DELETE. I still wonder if I could send a version number in the body. Generally, I don't understand the reasons a body should not be generated for DELETE? Why is it evil?

I understand that If-Match with ETag is the recommended solution to handle concurrency, but it can't handle basic collections. Why is there a 409 error code for concurrency issues, when the recommended solution ETag can't use it to be compliant? This is really confusing.

Keeping the version number in the body for concurrency detection would be consistent, but it does not work for DELETE.

Edited:

The above text was edited for clarification reflecting Everts comments.

I see the following options (even GET can be conditional I'm here only concerned about concurrency update issues):

A) Client requests use If-Match header. Server responses put ETag in header and in body

The ETag in the body gives the client a consistent way of handling single resources and collections. In case the collection needs to have a version on its own the ETag in the header is available and can also be used for conditional GET.

I have to give up sending 409 for concurrency errors.

B) Version number is sent in the body except for DELETE.

In the case of DELETE the version number must be either sent with a query parameter or with an If-Match header. It is not pleasant to have this exception, but a more clear 409 can be sent and single resources and collections are handled consistently.

I'm still unsure about what implementation to choose. Are there any clear criteria that can help with the decision? How has this problem been solved by others?

Do I misunderstand the usage of 409?

Update:

This German article https://www.seoagentur.de/seo-handbuch/lexikon/s/statuscode-412-precondition-failed/ explains the usage of 409 vs 412:

412 should only be used if the precondition (e.g. If-Match) caused the failure, while 409 should be used if the entity would cause a conflict. If a database with version id is used this id can be passed to the ETag and from the If-Match back to the version id. However, it is not forbidden to do it in the body of the entity itself. It just requires that the concept and how it works be explained, whereas the ETag solution just lets you refer to the HTTP specification. (comment: it still leaves the no body in DELETE problem)

The ETag with collections problem has more nuances:

a) If the returned collection is just data for a list view it normally doesn't hold all data of the displayed objects. If one item in the list is edited the client needs to first GET the full entity from a single resource URI and will get the required ETag with that request. It also provides are current version of the resource at the time of editing and not when the list was requested.

b) If the returned collection holds the full entity data and performance issues are relevant or many items independently need to be changed at once, than the ETags of each item can be passed to the client in the body.

Update 2:

The "Zalando RESTfull API and Event Guidelines" compare various options for optimisitc locking at [https://opensource.zalando.com/restful-api-guidelines/#optimistic-locking][2]

They favour sending the ETag in the body for server responses.

I currently think that this is the best option. The client than has the freedom to use that ETag or send a new Get on the single resource.


Solution

  • To me "Conflict 409" is the right response for a concurrency error, but the RFC states that a "Precondition Fails 412" needs to be send, which is not clear enough. Of course an error description in the body can clarify the cause of the error, but I would rather send 409.

    Use the HTTP status codes as they are defined, not what you think they should mean based on the name.

    When a collection (e.g. all due orders) is returned from the server there is no possibility to send multiple ETags. Each single resource can be individually modified. Therefore the only option is to send the ETag in the body as a property of each representation in the JSON response. It is confusing that collections are treated differently than single resources. Also the client needs to include a property for the ETag in its object model in any case.

    HTTP doesn't really have a concept of collections, but you can give the collection itself its own ETag as well.

    I don't understand all the implications of the specific wording. What does "has no defined semantics" mean? What is the reasons a body should not be generated?

    A delete request should only delete the resource located at the URI. If it's successful, we can assume that the URI that was used in the DELETE request will return a 404 or 410 after the request was successful.

    If you want to conditionally parameterize deletions, delete multiple resources at once or delete something other than the resource specified in the URI, the DELETE request is simply not appropriate for that use-case.

    If you want to use ETags, just use an If-Match header.