HTTP

Status Code

428 Precondition Required

The server requires the request to be conditional. Learn when to use 428 Precondition Required to prevent lost updates and race conditions.

6 min read intermediate Try in Playground

TL;DR: Server requires conditional headers (like If-Match) to prevent lost updates in concurrent editing. Get the resource’s ETag first, then include it in your update request.

What is 428 Precondition Required?

A 428 Precondition Required status code means the server requires the request to include conditional headers before it can be processed. Think of it like a bank requiring you to provide your current account balance before approving a withdrawal—it ensures you’re working with up-to-date information and prevents conflicting changes.

This status code is used to prevent the “lost update problem” where multiple clients might modify a resource simultaneously, potentially overwriting each other’s changes.

When Does This Happen?

You’ll see a 428 Precondition Required response in these common situations:

1. Updating Shared Resources

Multiple users editing the same document
→ Server requires If-Match header
→ Ensures you have the latest version
→ Prevents overwriting others' changes

2. Critical Data Modifications

Updating financial records
→ Requires If-Unmodified-Since
→ Confirms data hasn't changed
→ Prevents race conditions

3. API Rate Limiting Protection

High-frequency API endpoint
→ Requires conditional headers
→ Prevents accidental duplicate requests
→ Protects server resources

4. Cache Validation Required

Critical resource update
→ Server demands If-None-Match
→ Ensures client has current version
→ Prevents stale data updates

5. Distributed System Coordination

Microservices updating shared state
→ Requires version headers
→ Maintains consistency
→ Prevents conflicts

Example Responses

Basic 428 Response:

HTTP/1.1 428 Precondition Required
Content-Type: application/json
Content-Length: 156

{
  "error": "Precondition Required",
  "message": "This request must be conditional. Please include an If-Match or If-Unmodified-Since header.",
  "required_headers": ["If-Match", "If-Unmodified-Since"]
}
```text

**With ETag Information:**

```http
HTTP/1.1 428 Precondition Required
Content-Type: application/json
ETag: "33a64df551425fcc55e4d42a148795d9f25f89d4"

{
  "error": "Precondition Required",
  "message": "To update this resource, include an If-Match header with the current ETag",
  "current_etag": "33a64df551425fcc55e4d42a148795d9f25f89d4",
  "example": "If-Match: \"33a64df551425fcc55e4d42a148795d9f25f89d4\""
}

Detailed Guidance:

HTTP/1.1 428 Precondition Required
Content-Type: application/json
Link: <https://api.example.com/docs/conditional-requests>; rel="help"

{
  "error": "Precondition Required",
  "message": "This endpoint requires conditional request headers to prevent lost updates",
  "details": {
    "reason": "Resource can be modified by multiple clients",
    "required_headers": [
      {
        "name": "If-Match",
        "description": "Include the ETag from your last GET request",
        "example": "If-Match: \"abc123\""
      },
      {
        "name": "If-Unmodified-Since",
        "description": "Include the Last-Modified date from your last GET request",
        "example": "If-Unmodified-Since: Wed, 21 Oct 2025 07:28:00 GMT"
      }
    ]
  },
  "documentation": "https://api.example.com/docs/conditional-requests"
}
```text

## Real-World Example

Imagine you're updating a user profile through an API:

**Initial Request (Missing Precondition):**

```http
PUT /api/users/12345 HTTP/1.1
Host: api.example.com
Content-Type: application/json
Authorization: Bearer eyJhbGciOiJIUzI1NiIs...

{
  "name": "John Doe",
  "email": "john.doe@example.com",
  "role": "admin"
}

428 Response:

HTTP/1.1 428 Precondition Required
Content-Type: application/json
Cache-Control: no-cache
Content-Length: 423

{
  "error": "Precondition Required",
  "message": "User profile updates require conditional headers to prevent conflicts",
  "details": {
    "reason": "This resource can be modified by the user and administrators simultaneously",
    "solution": "First, GET the current resource to obtain the ETag, then include it in your PUT request"
  },
  "steps": [
    "1. GET /api/users/12345 to retrieve current data and ETag",
    "2. Include 'If-Match: <etag>' header in your PUT request",
    "3. Retry your PUT request with the If-Match header"
  ],
  "example": {
    "header": "If-Match",
    "value": "\"33a64df551425fcc55e4d42a148795d9f25f89d4\""
  }
}
```text

**Corrected Request Flow:**

**Step 1: Get Current Resource**

```http
GET /api/users/12345 HTTP/1.1
Host: api.example.com
Authorization: Bearer eyJhbGciOiJIUzI1NiIs...

Response:
HTTP/1.1 200 OK
ETag: "v1.2.3"
Last-Modified: Wed, 18 Jan 2026 10:00:00 GMT

{
  "id": "12345",
  "name": "John Smith",
  "email": "john.smith@example.com",
  "role": "user"
}

Step 2: Update With Precondition

PUT /api/users/12345 HTTP/1.1
Host: api.example.com
Content-Type: application/json
Authorization: Bearer eyJhbGciOiJIUzI1NiIs...
If-Match: "v1.2.3"

{
  "name": "John Doe",
  "email": "john.doe@example.com",
  "role": "admin"
}

Response:
HTTP/1.1 200 OK
ETag: "v1.2.4"

{
  "id": "12345",
  "name": "John Doe",
  "email": "john.doe@example.com",
  "role": "admin",
  "updated_at": "2026-01-18T10:05:00Z"
}
```text

## 428 vs Other Conditional Codes

| Code    | Meaning               | Scenario                               | Solution                            |
| ------- | --------------------- | -------------------------------------- | ----------------------------------- |
| **428** | Precondition Required | No conditional header provided         | Add If-Match or If-Unmodified-Since |
| **412** | Precondition Failed   | Conditional header provided but failed | Get latest version and retry        |
| **409** | Conflict              | Request conflicts with current state   | Resolve conflict manually           |
| **304** | Not Modified          | Conditional GET shows no change        | Use cached version                  |

## Important Characteristics

**Prevents Lost Updates:**

```text
User A reads resource (version 1)
User B reads resource (version 1)
User A updates to version 2
User B updates (would overwrite A's changes)
428 forces B to get version 2 first
```text

**Server Policy Decision:**

```http
HTTP/1.1 428 Precondition Required

Server decides which endpoints require conditions

Common Required Headers:

  • If-Match: Requires specific ETag
  • If-Unmodified-Since: Requires resource unchanged since date
  • If-None-Match: For conditional creation
  • Custom version headers

Common Mistakes

❌ Not providing guidance

HTTP/1.1 428 Precondition Required

Precondition Required  ← Unhelpful, what precondition?
```text

**❌ Requiring conditions on read-only requests**

```http
GET /api/users/123

HTTP/1.1 428 Precondition Required  ← Bad: GET shouldn't require conditions

❌ Not documenting which headers are needed

{
  "error": "Precondition Required"
 Missing: which headers? which values?
}
```text

**✅ Correct usage**

```http
HTTP/1.1 428 Precondition Required
Content-Type: application/json

{
  "error": "Precondition Required",
  "message": "Include If-Match header with current ETag",
  "required_header": "If-Match",
  "get_etag_from": "GET /api/users/123",
  "current_etag": "\"abc123\""
}

Best Practices

Provide Current ETag:

app.put('/api/resource/:id', async (req, res) => {
  if (!req.headers['if-match']) {
    const resource = await db.getResource(req.params.id)

    return res.status(428).json({
      error: 'Precondition Required',
      message: 'Include If-Match header to update this resource',
      current_etag: resource.etag,
      example: `If-Match: "${resource.etag}"`
    })
  }

  // Process conditional update...
})
```text

**Document Required Headers:**

```http
HTTP/1.1 428 Precondition Required
Content-Type: application/json

{
  "error": "Precondition Required",
  "required_headers": {
    "If-Match": {
      "description": "ETag from your last GET request",
      "example": "If-Match: \"v1.2.3\"",
      "obtain_from": "GET /api/resource/:id"
    }
  },
  "documentation": "https://docs.example.com/conditional-requests"
}

Apply to Critical Endpoints Only:

const requiresPrecondition = [
  'PUT /api/users/:id',
  'PATCH /api/documents/:id',
  'DELETE /api/critical-data/:id'
]

// Don't require for less critical operations
const optional = ['POST /api/logs', 'GET /api/*', 'POST /api/analytics']
```javascript

**Consistent Error Format:**

```javascript
function preconditionRequiredError(resourceType, etag) {
  return {
    error: 'Precondition Required',
    message: `${resourceType} updates require conditional headers`,
    required_headers: ['If-Match', 'If-Unmodified-Since'],
    current_etag: etag,
    steps: [
      `1. GET the current ${resourceType}`,
      '2. Note the ETag header in the response',
      '3. Include If-Match: <etag> in your update request'
    ]
  }
}

Implementation Examples

Express.js Middleware:

const requireConditional = (req, res, next) => {
  const requiresCondition = ['PUT', 'PATCH', 'DELETE'].includes(req.method)
  const hasCondition = req.headers['if-match'] || req.headers['if-unmodified-since']

  if (requiresCondition && !hasCondition) {
    return res.status(428).json({
      error: 'Precondition Required',
      message: 'This operation requires a conditional header',
      required_headers: ['If-Match', 'If-Unmodified-Since'],
      hint: `GET ${req.path} to obtain the current ETag`
    })
  }

  next()
}

app.put('/api/users/:id', requireConditional, async (req, res) => {
  // Handle conditional update...
})
```javascript

**Django:**

```python
from django.http import JsonResponse
from django.views.decorators.http import require_http_methods

@require_http_methods(["PUT"])
def update_user(request, user_id):
    if_match = request.headers.get('If-Match')

    if not if_match:
        return JsonResponse({
            'error': 'Precondition Required',
            'message': 'Include If-Match header with current ETag',
            'required_header': 'If-Match',
            'get_etag_from': f'/api/users/{user_id}'
        }, status=428)

    # Process conditional update
    user = User.objects.get(id=user_id)

    if user.etag != if_match.strip('"'):
        return JsonResponse({
            'error': 'Precondition Failed'
        }, status=412)

    # Update user...

Ruby on Rails:

class UsersController < ApplicationController
  before_action :require_precondition, only: [:update, :destroy]

  def update
    @user = User.find(params[:id])

    if stale?(etag: @user, last_modified: @user.updated_at)
      if @user.update(user_params)
        render json: @user
      else
        render json: @user.errors, status: :unprocessable_entity
      end
    end
  end

  private

  def require_precondition
    unless request.headers['If-Match'] || request.headers['If-Unmodified-Since']
      render json: {
        error: 'Precondition Required',
        message: 'Include If-Match or If-Unmodified-Since header',
        required_headers: ['If-Match', 'If-Unmodified-Since']
      }, status: 428
    end
  end
end
```javascript

## Client-Side Handling

**JavaScript Fetch API:**

```javascript
async function updateUser(userId, updates) {
  // First, get current resource
  const getResponse = await fetch(`/api/users/${userId}`)
  const etag = getResponse.headers.get('ETag')

  // Then update with If-Match
  const putResponse = await fetch(`/api/users/${userId}`, {
    method: 'PUT',
    headers: {
      'Content-Type': 'application/json',
      'If-Match': etag
    },
    body: JSON.stringify(updates)
  })

  if (putResponse.status === 428) {
    throw new Error('Precondition required - this should not happen!')
  }

  if (putResponse.status === 412) {
    // Resource changed, need to refetch
    console.log('Resource was modified, retrying...')
    return updateUser(userId, updates) // Retry
  }

  return putResponse.json()
}

Try It Yourself

Visit our request builder and see 428 Precondition Required:

  1. Set method to PUT
  2. Set path to /api/protected-resource
  3. Click Send request (without If-Match header)
  4. Observe 428 response
  5. Add If-Match header and retry

Frequently Asked Questions

What does 428 Precondition Required mean?

A 428 error means the server requires the request to include conditional headers like If-Match or If-Unmodified-Since. This prevents the lost update problem in concurrent editing scenarios.

How do I fix a 428 error?

First GET the resource to obtain its current ETag, then include If-Match header with that ETag in your PUT or PATCH request. This proves you are updating the version you intended to modify.

What is the lost update problem?

When two users edit the same resource simultaneously, one update can overwrite the other. Requiring preconditions ensures updates only succeed if the resource has not changed since it was read.

What is the difference between 428 and 412?

428 means you must include precondition headers but did not. 412 means you included precondition headers but they failed. 428 is missing headers; 412 is failed conditions.

Keep Learning