Introduction
In this post I'm going to focus on the problem related to HTTP PUT method. I will show by example Web API HTTP PUT method which can lead to bugs in the code and show you how to handle such scenarios to make your code reliable. At the bottom of the post, there is a github repository with all codebase used here.
How should we handle updating of the data in API?
Probably the most common way to update the data in API is usage of PUT HTTP method.
Let's take a look at the code:
public class Customer
{
public int Id { get; set; }
public string Name { get; set; }
public string City { get; set; }
public bool IsPremiumMember { get; set; }
}
Let's assume that we develop a web-shop application. When user spends some particular amount of money, she or he will become a premium member and has the opportunity to purchase some goods with the extra discount. For sake of simplicity we put some good practices like domain-centric logic aside. In our example the shop is very young startup with only critical functions in their system and every update on the data is done manually by stuff. To update the customer's data, we need to trigger a Put method of the controller class:
[ApiController]
[Route("[controller]")]
public class CustomerController : ControllerBase
{
private static readonly IEnumerable<Customer> _customers = new List<Customer>()
{
new Customer { Id = 1, Name = "John", City = "Cracow", IsPremiumMember = false }
};
[HttpGet("{id}")]
public IActionResult Get(int id)
{
return Ok(_customers.FirstOrDefault(x => x.Id == id));
}
[HttpPut]
public IActionResult Put(Customer customer)
{
var toUpdate = _customers.First(x => x.Id == customer.Id);
toUpdate.Name = customer.Name;
toUpdate.City = customer.City;
toUpdate.IsPremiumMember = customer.IsPremiumMember;
return NoContent();
}
}
John has moved recently to Warsaw. He's calling to the shop and tells that fact. Bob, who is a worker of the shop is picking up the phone and is going to update John's address as requested. He opens a web browser with the application. The frontend layer calls backend get method and gets the data:
{
"id": 1,
"name": "John",
"city": "Cracow",
"isPremiumMember": false
}
Bob is just going to update the data but before that he wants to make a coffee.
At the same time Alice who works in the same team as Bob needs to perform an update operation on Johns account as well. We know that John spends a lot of money and his account needs to be promoted to premium level. Alice opens the browser and gets the same data. She clicks 'Promote account to premium' button and updates the data. After refreshing a browser she gets the data:
{
"id": 1,
"name": "John",
"city": "Cracow",
"isPremiumMember": true
}
Bob goes back to his PC with a mug of coffee and he's going to finish his task. He amends the city to Warsaw and clicks update button. What will be the result of such scenario? Let's check that and call Get method. The result is:
{
"id": 1,
"name": "John",
"city": "Warsaw",
"isPremiumMember": false
}
What?! John is not a premium user? You may ask: how it is possible? It can happen because PUT performs update on all fields. We send to backend a set of data and we update every single field except the id. The newly updated field can be overwritten with the old value like in Alice and Bob case. We don't want to have such vulnerability in our code. Let's check what we can do in order to mitigate this issue.
Versioning
The first method to solve the issue is versioning. We need to assign initial version to John's data and increment that after every update. Let's change the code and use this mechanism:
public abstract class VersionedEntity
{
public long Version { get; set; }
}
public class Customer : VersionedEntity
{
public int Id { get; set; }
public string Name { get; set; }
public string City { get; set; }
public bool IsPremiumMember { get; set; }
}
We created VersionedEntity abstract class because if we create any new entity it will be easier to maintain solution but it is not required. We could add Version property directly to Customer class.
OK, we have a version stored in the system, let's try to reproduce Alice & Bob scenario and check if the issue still exists.
- Bob makes GET call to fetch the data, a result is:
{
"id": 1,
"name": "John",
"city": "Cracow",
"isPremiumMember": false,
"version": 1
}
- Bob goes to make a coffee
- Alice opens a browser, gets the same data from GET method.
- Alice updates the data, she sets 'isPremiumMember' to 'true', refreshes the browser to get the data:
{
"id": 1,
"name": "John",
"city": "Cracow",
"isPremiumMember": true,
"version": 2 // note that version was incremented
}
- Bob is back. He wants to amend the city from 'Cracow' to 'Warsaw'. After clicking 'Update' (his browser has still data with version: 1) button he gets the message:
The data you're trying to update has been changed. Please reload data and try once again.
All right! The issue does not exist! How did we achieved that? Apart from adding the version to the Customer class, we needed to change a controller method as well:
[HttpPut]
public IActionResult Put(Customer customer)
{
var toUpdate = _customers.First(x => x.Id == customer.Id);
if (toUpdate.Version != customer.Version)
{
return BadRequest(
"The data you're trying to update has been changed. " +
"Please reload data and try once again.");
}
toUpdate.Name = customer.Name;
toUpdate.City = customer.City;
toUpdate.IsPremiumMember = customer.IsPremiumMember;
toUpdate.Version++;
return NoContent();
}
Before updating we check if passed version of the data is the latest. If not, a bad request with a proper message is returned. That's it! Let's go to the next way of handing the 'put issue'.
PATCH instead of PUT
The problem related to PUT is that it updated all fields. Not only changed but all of them. PATCH has different concept it applies only changes without touching unchanged parts. To get more information about patch please take a look at the site: http://jsonpatch.com/. To replace PUT with PATCH in our example we need to add two nuget packages to our solution:
<PackageReference Include="Microsoft.AspNetCore.JsonPatch" Version="5.0.1" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="5.0.1" />
The next step is to add Newtonsoft Json to our services in Startup class:
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers()
.AddNewtonsoftJson();
services.AddSwaggerGen(c =>
{
c.SwaggerDoc("v1", new OpenApiInfo { Title = "Blog", Version = "v1" });
});
}
The final step is replacing Put with Patch method in controller class:
[HttpPatch("{id}")]
public IActionResult Patch(int id, JsonPatchDocument<Customer> patchDoc)
{
var toUpdate = _customers.First(x => x.Id == id);
patchDoc.ApplyTo(toUpdate);
return NoContent();
}
To generate PATCH operation you can use additional libraries for your frontend application (you can find them on linked website). For this article purpose I used online generator https://extendsclass.com/json-patch.html.
Since PATCH does not touch unchanged data we are good to perform our two updates at the same time. The request body to update the city is:
[
{
"op": "replace",
"path": "/city",
"value": "Warsaw"
}
]
And promoting to premium account:
[
{
"op": "replace",
"path": "/isPremiumMember",
"value": true
}
]
As you can see it is maybe not so simple as PUT method but it provides better user experience - there is no need of refresh before updating if the data has been updated in the meantime.
A summary:
- HTTP PUT performs update of all fields, not only on changed ones. That may lead to case that the newly updated field is overwritten with the old value,
- To mitigate the issue you can apply versioning of the data. If user tries to update newer data with older data, an error is returned,
- To resolve problem you can use PATCH instead of PUT. It is a bit more complicated and requires more code. It is worth doing that if we can have multiple concurrent updates on our data.
Thank you for reading to the end! I hope that my tips will help you to create more reliable code.
If you like my posts, please follow me on Twitter :)
Github repository is available here (a postman collection with all http requests is attached to the repo):
The problem with HTTP PUT method and possible ways to resolve it
Demo code for article on dev.to website: https://dev.to/pnieweglowski/the-problem-with-http-put-method-and-possible-ways-to-resolve-it-3p5h
Repository contains code used in the blog post as well as postman collection with request to fetch / modify data.
Top comments (0)