go 59 lines · 9 steps

Optimistic concurrency with If-Match in Gin

A Gin handler that uses the If-Match header and version numbers to reject lost-update writes.

Explained by highlit
1func UpdateArticle(store *ArticleStore) gin.HandlerFunc {
2 return func(c *gin.Context) {
3 id := c.Param("id")
4 
5 ifMatch := strings.Trim(c.GetHeader("If-Match"), `"`)
6 if ifMatch == "" {
7 c.JSON(http.StatusPreconditionRequired, gin.H{"error": "If-Match header is required"})
8 return
9 }
10 
11 expectedVersion, err := strconv.ParseInt(ifMatch, 10, 64)
12 if err != nil {
13 c.JSON(http.StatusBadRequest, gin.H{"error": "invalid If-Match version"})
14 return
15 }
16 
17 var payload struct {
18 Title string `json:"title" binding:"required"`
19 Body string `json:"body" binding:"required"`
20 }
21 if err := c.ShouldBindJSON(&payload); err != nil {
22 c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
23 return
24 }
25 
26 article, err := store.FindByID(c.Request.Context(), id)
27 if errors.Is(err, ErrNotFound) {
28 c.JSON(http.StatusNotFound, gin.H{"error": "article not found"})
29 return
30 } else if err != nil {
31 c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to load article"})
32 return
33 }
34 
35 if article.Version != expectedVersion {
36 c.Header("ETag", strconv.FormatInt(article.Version, 10))
37 c.JSON(http.StatusPreconditionFailed, gin.H{
38 "error": "article was modified by another request",
39 "current_version": article.Version,
40 })
41 return
42 }
43 
44 article.Title = payload.Title
45 article.Body = payload.Body
46 
47 updated, err := store.UpdateWithVersion(c.Request.Context(), article, expectedVersion)
48 if errors.Is(err, ErrVersionConflict) {
49 c.JSON(http.StatusPreconditionFailed, gin.H{"error": "article was modified concurrently"})
50 return
51 } else if err != nil {
52 c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to update article"})
53 return
54 }
55 
56 c.Header("ETag", strconv.FormatInt(updated.Version, 10))
57 c.JSON(http.StatusOK, updated)
58 }
59}
01 / 01
STEP 01

Walkthrough

Space play step click any line
Three takeaways
  1. 1The If-Match header carries a client's expected version so the server can reject stale writes.
  2. 2Comparing versions before and during the update guards against lost updates from concurrent requests.
  3. 3Returning the current version in a 412 response lets clients retry with fresh state.

Related explainers

Share this explainer

Here's the card — post it anywhere.

Optimistic concurrency with If-Match in Gin — share card
Made with highlit — turn any snippet into a walkthrough like this in about a minute.
Explain your code