REST Lens
← Back to All Rules
Rule 60

Conditional PUT

Category: Concurrency Control

Description

PUT requests should support conditional request headers (If-Unmodified-Since or If-Match) to prevent lost updates in concurrent scenarios.

What It Checks

This rule validates that PUT operations include at least one of the following conditional request headers:

  • If-Unmodified-Since: Allows the request only if the resource hasn't been modified since the specified date
  • If-Match: Allows the request only if the resource's ETag matches the provided value

The rule checks:

  • All PUT operations that include a request body
  • Verifies that at least one conditional header parameter is defined
  • Case-insensitive header name matching (both If-Match and if-match are accepted)

Why It Matters

Conditional headers are critical for preventing the "lost update problem" in distributed systems:

  • Concurrency safety: Prevents two clients from overwriting each other's changes without knowing it
  • Data integrity: Ensures updates are only applied when the client has the current version of the resource
  • Optimistic locking: Enables efficient concurrency control without database locks
  • HTTP standard compliance: Follows RFC 7232 specifications for conditional requests

The Lost Update Problem

Without conditional headers, consider this scenario:

  1. Client A reads resource (version 1)
  2. Client B reads resource (version 1)
  3. Client A updates resource (version 2)
  4. Client B updates resource based on old version 1 → Client A's changes are lost!

Conditional headers prevent this by failing Client B's request with 412 Precondition Failed.

Examples

Good ✅

paths:
  /users/{userId}:
    put:
      summary: Update a user
      parameters:
        - name: userId
          in: path
          required: true
          schema:
            type: string
        - name: If-Match
          in: header
          required: false
          description: ETag from previous GET request
          schema:
            type: string
        - name: If-Unmodified-Since
          in: header
          required: false
          description: RFC 7231 date-time from Last-Modified header
          schema:
            type: string
            format: date-time
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/User'
      responses:
        '200':
          description: User updated successfully
        '412':
          description: Precondition failed - resource was modified

Using only If-Match (also valid) ✅

paths:
  /articles/{articleId}:
    put:
      summary: Update an article
      parameters:
        - name: articleId
          in: path
          required: true
          schema:
            type: string
        - name: If-Match
          in: header
          required: true  # Can be required or optional
          description: ETag value (e.g., "33a64df551425fcc55e4d42a148795d9f25f89d4")
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/Article'
      responses:
        '200':
          description: Article updated
          headers:
            ETag:
              description: New ETag of the updated resource
              schema:
                type: string
        '412':
          description: Precondition failed

Bad ❌

paths:
  /users/{userId}:
    put:
      summary: Update a user
      parameters:
        - name: userId
          in: path
          required: true
          schema:
            type: string
        # Missing If-Match or If-Unmodified-Since!
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/User'
      responses:
        '200':
          description: User updated

Another Bad Example ❌

paths:
  /products/{productId}:
    put:
      summary: Update product
      parameters:
        - name: productId
          in: path
          required: true
          schema:
            type: string
        - name: X-Custom-Version  # Custom headers don't count
          in: header
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/Product'
      responses:
        '200':
          description: Product updated

Configuration

This rule has no configuration options. It is either enabled or disabled.

When enabled, the rule requires at least one of the standard HTTP conditional headers (If-Unmodified-Since or If-Match) to be present on PUT operations.

How to Fix

Step 1: Choose Your Conditional Header Strategy

Option A: Use If-Match with ETags (Recommended)

  • More robust and reliable
  • Works with any type of change (content or metadata)
  • Industry best practice

Option B: Use If-Unmodified-Since with Last-Modified

  • Simpler to implement
  • Only detects content changes
  • Limited to 1-second precision

Option C: Support both (Best)

  • Provides maximum flexibility for clients
  • Allows clients to choose their preferred method

Step 2: Add the Header Parameter

Add the conditional header to your PUT operation's parameters:

parameters:
  - name: If-Match
    in: header
    required: false  # Can be true if you want to enforce it
    description: |
      ETag value from a previous GET request. Request fails with 412
      if the resource has been modified since.
    schema:
      type: string
    example: '"33a64df551425fcc55e4d42a148795d9f25f89d4"'

Or for If-Unmodified-Since:

parameters:
  - name: If-Unmodified-Since
    in: header
    required: false
    description: |
      RFC 7231 date-time. Request succeeds only if the resource
      has not been modified since this date.
    schema:
      type: string
      format: date-time
    example: "Wed, 21 Oct 2015 07:28:00 GMT"

Step 3: Document the 412 Response

Add a 412 Precondition Failed response to indicate when the conditional check fails:

responses:
  '200':
    description: Resource updated successfully
    headers:
      ETag:  # Return new ETag for next update
        schema:
          type: string
  '412':
    description: |
      Precondition Failed - The resource was modified by another client.
      Fetch the latest version and retry your update.
    content:
      application/json:
        schema:
          $ref: '#/components/schemas/Error'

Step 4: Update GET to Return Matching Headers

Ensure your GET operations return the corresponding headers:

For If-Match, return ETag:

/users/{userId}:
  get:
    responses:
      '200':
        description: User details
        headers:
          ETag:
            description: Version identifier
            schema:
              type: string

For If-Unmodified-Since, return Last-Modified:

/users/{userId}:
  get:
    responses:
      '200':
        description: User details
        headers:
          Last-Modified:
            description: Last modification time
            schema:
              type: string
              format: date-time

Step 5: Implement Server-Side Logic

Your API implementation must:

  1. Store and return ETags or Last-Modified timestamps
  2. Check the conditional header on PUT requests
  3. Return 412 Precondition Failed if the condition fails
  4. Only apply the update if the condition passes

Related Rules

  • Rule 58: Last-Modified - Ensures GET responses include Last-Modified header for cache control
  • Rule 53: Create/Update Return - Ensures PUT operations return the updated resource
  • Rule 34: Code 201 Create - Related to proper status codes for resource creation