Definition
Unsafe methods are HTTP methods that are designed to modify server state - they create, update, or delete resources. Unlike safe methods (GET, HEAD, OPTIONS) which are read-only, unsafe methods have side effects that change the server’s data or state. The four primary unsafe methods are POST, PUT, PATCH, and DELETE.
The term “unsafe” doesn’t mean dangerous or insecure - it means these methods are not safe to retry blindly without consequences. If you accidentally refresh a page after submitting a form with POST, you might create a duplicate order or send a duplicate email. This is why browsers show “Are you sure you want to resubmit this form?” warnings for POST requests.
Because unsafe methods modify state, they require more careful handling: proper authentication and authorization, CSRF protection, idempotency keys for retries, rate limiting to prevent abuse, and audit logging. While PUT and DELETE are idempotent (safe to retry), POST and PATCH generally are not, making retry logic more complex. Unsafe methods are the workhorses of REST APIs - they’re how you actually DO things rather than just reading information.
Example
POST - Create Resource: When you sign up for an account, the browser sends POST /api/users with your email and password. This creates a new user record. Sending the same POST again creates another duplicate user (unless the API implements idempotency checks).
PUT - Replace Resource: When you edit your profile and save, the client sends PUT /api/users/123 with all your profile fields. This completely replaces the user resource. PUT is idempotent - sending it 5 times results in the same profile data.
PATCH - Partial Update: To change just your email address, the client sends PATCH /api/users/123 with {"email": "[email protected]"}. This updates only the email field. PATCH can be idempotent if designed carefully (absolute values) but often isn’t (incremental changes).
DELETE - Remove Resource: When you delete a post, the client sends DELETE /api/posts/456. This removes the post. DELETE is idempotent - deleting the same post 10 times results in the post being gone (subsequent attempts return 404).
E-commerce Checkout: When you click “Place Order,” the client sends POST /api/orders with cart items and shipping info. This creates an order, decrements inventory, and charges your card. Accidentally refreshing would create a duplicate order - that’s why payment systems use idempotency keys.
Social Media Like Button: Clicking “like” on a post might send POST /api/posts/789/likes. This creates a like record. Clicking again could unlike (toggle) or create a duplicate. Well-designed APIs make this idempotent: POST creates like if it doesn’t exist, returns existing like if it does.
File Upload: Uploading a file sends PUT /api/files/photo.jpg with the file content. PUT is idempotent - uploading the same file multiple times results in the same file being stored, overwriting previous versions.
Analogy
Writing in a Notebook: When you write something in a notebook, you change it permanently. You can’t “undo” writing by reading the page. POST, PUT, PATCH, and DELETE are like writing - they modify the state and have lasting effects.
Submitting a Form: When you mail a physical form (like a tax return), you can’t un-submit it by opening your mailbox and looking inside. Submission (POST) is an unsafe operation with consequences. Reading your mailbox (GET) is safe.
Making a Purchase: Buying something at a store changes state - money leaves your account, inventory decreases, a transaction is recorded. This is an unsafe operation. Browsing products is safe, but “Add to Cart” and “Checkout” are unsafe operations.
Editing a Whiteboard: Erasing and rewriting a whiteboard changes its state permanently. This is like PUT (replace) or PATCH (update part of it). Looking at the whiteboard (GET) doesn’t change it.
Deleting a File: When you delete a file from your computer, it’s gone (or in trash). You can delete it multiple times, but after the first time it’s already gone - similar to how DELETE is idempotent. Reading the file (GET) doesn’t delete it.
Code Example
# ===== POST - Create Resource (NOT Idempotent) =====
# Each request creates a new resource
POST /api/orders HTTP/1.1
Host: api.example.com
Content-Type: application/json
Authorization: Bearer token123
{
"product_id": 456,
"quantity": 2,
"shipping_address": "123 Main St"
}
# Response
HTTP/1.1 201 Created
Location: /api/orders/789
Content-Type: application/json
{
"id": 789,
"product_id": 456,
"quantity": 2,
"total": 39.98,
"status": "pending"
}
# Retry creates ANOTHER order (id: 790) - NOT idempotent
# Solution: Use Idempotency-Key header
POST /api/orders HTTP/1.1
Host: api.example.com
Content-Type: application/json
Authorization: Bearer token123
Idempotency-Key: 550e8400-e29b-41d4-a716-446655440000
{
"product_id": 456,
"quantity": 2
}
# Retry with same key returns original order (id: 789) - NOW idempotent
# ===== PUT - Replace Resource (Idempotent) =====
# Replaces entire resource
PUT /api/products/456 HTTP/1.1
Host: api.example.com
Content-Type: application/json
Authorization: Bearer token123
{
"name": "Updated Widget",
"price": 12.99,
"stock": 150,
"description": "New description"
}
# Response
HTTP/1.1 200 OK
Content-Type: application/json
{
"id": 456,
"name": "Updated Widget",
"price": 12.99,
"stock": 150,
"description": "New description"
}
# Retry with same data - resource ends up in same state (idempotent)
# ===== PATCH - Partial Update (Usually NOT Idempotent) =====
# Updates only specified fields
PATCH /api/users/123 HTTP/1.1
Host: api.example.com
Content-Type: application/json
Authorization: Bearer token123
{
"email": "[email protected]"
}
# Response
HTTP/1.1 200 OK
Content-Type: application/json
{
"id": 123,
"name": "John Doe",
"email": "[email protected]"
}
# Idempotent if setting absolute values
# NOT idempotent if using operations like increment:
PATCH /api/products/456 HTTP/1.1
Content-Type: application/json
{
"stock": { "$inc": -1 } # Decrement by 1
}
# Retry decrements again - NOT idempotent!
# ===== DELETE - Remove Resource (Idempotent) =====
# First request
DELETE /api/posts/789 HTTP/1.1
Host: api.example.com
Authorization: Bearer token123
# Response
HTTP/1.1 204 No Content
# Retry - resource already gone
DELETE /api/posts/789 HTTP/1.1
Host: api.example.com
Authorization: Bearer token123
# Response (may be 404 or 204, both acceptable)
HTTP/1.1 404 Not Found
# Result is the same: resource doesn't exist (idempotent)
// JavaScript examples of unsafe methods
// ===== POST - Create Resource =====
async function createOrder(productId, quantity) {
const response = await fetch('/api/orders', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
},
body: JSON.stringify({
product_id: productId,
quantity: quantity
})
});
if (response.ok) {
const order = await response.json();
console.log('Order created:', order.id);
return order;
}
throw new Error('Failed to create order');
}
// Calling twice creates TWO orders (not safe to retry)
await createOrder(456, 2); // Creates order #789
await createOrder(456, 2); // Creates order #790 (duplicate!)
// POST with Idempotency Key
async function createOrderSafely(productId, quantity, idempotencyKey) {
const response = await fetch('/api/orders', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`,
'Idempotency-Key': idempotencyKey
},
body: JSON.stringify({
product_id: productId,
quantity: quantity
})
});
return response.json();
}
const key = crypto.randomUUID();
await createOrderSafely(456, 2, key); // Creates order #789
await createOrderSafely(456, 2, key); // Returns order #789 (same one)
// ===== PUT - Replace Resource =====
async function updateProduct(id, productData) {
const response = await fetch(`/api/products/${id}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
},
body: JSON.stringify(productData)
});
return response.json();
}
// PUT is idempotent - safe to retry
const product = {
name: 'Updated Widget',
price: 12.99,
stock: 150
};
await updateProduct(456, product); // Product updated
await updateProduct(456, product); // Same result (idempotent)
// ===== PATCH - Partial Update =====
async function updateUserEmail(userId, newEmail) {
const response = await fetch(`/api/users/${userId}`, {
method: 'PATCH',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
},
body: JSON.stringify({
email: newEmail
})
});
return response.json();
}
// Idempotent PATCH (absolute value)
await updateUserEmail(123, '[email protected]'); // Email changed
await updateUserEmail(123, '[email protected]'); // Email still '[email protected]' (idempotent)
// Non-Idempotent PATCH (increment operation)
async function incrementViewCount(postId) {
const response = await fetch(`/api/posts/${postId}`, {
method: 'PATCH',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
},
body: JSON.stringify({
views: { $inc: 1 }
})
});
return response.json();
}
await incrementViewCount(789); // Views: 101
await incrementViewCount(789); // Views: 102 (NOT idempotent!)
// ===== DELETE - Remove Resource =====
async function deletePost(postId) {
const response = await fetch(`/api/posts/${postId}`, {
method: 'DELETE',
headers: {
'Authorization': `Bearer ${token}`
}
});
if (response.status === 204 || response.status === 404) {
console.log('Post deleted or already gone');
return true;
}
throw new Error('Failed to delete post');
}
// DELETE is idempotent - safe to retry
await deletePost(789); // 204 No Content (deleted)
await deletePost(789); // 404 Not Found (already gone)
// Result is the same: post doesn't exist
// ===== Error Handling and Retry Logic =====
async function safeRetry(operation, maxRetries = 3) {
for (let i = 0; i < maxRetries; i++) {
try {
return await operation();
} catch (error) {
if (i === maxRetries - 1) throw error;
// Exponential backoff
const delay = Math.pow(2, i) * 1000;
await new Promise(resolve => setTimeout(resolve, delay));
}
}
}
// Safe to retry idempotent operations
await safeRetry(() => updateProduct(456, product)); // PUT - safe
await safeRetry(() => deletePost(789)); // DELETE - safe
// NOT safe to retry non-idempotent operations without idempotency key
// await safeRetry(() => createOrder(456, 2)); // DON'T DO THIS - creates duplicates!
// Safe with idempotency key
const idempKey = crypto.randomUUID();
await safeRetry(() => createOrderSafely(456, 2, idempKey)); // Safe now
Diagram
graph TB
subgraph "Unsafe Methods (Modify State)"
POST[POST
Create resource]
PUT[PUT
Replace resource]
PATCH[PATCH
Partial update]
DELETE[DELETE
Remove resource]
end
subgraph "Idempotency"
I1[POST: NOT idempotent
unless using idempotency keys]
I2[PUT: Idempotent
Safe to retry]
I3[PATCH: Usually NOT
idempotent if incremental]
I4[DELETE: Idempotent
Safe to retry]
end
POST --> I1
PUT --> I2
PATCH --> I3
DELETE --> I4
subgraph "Security Concerns"
S1[Authentication Required]
S2[Authorization Checks]
S3[CSRF Protection]
S4[Rate Limiting]
S5Audit [Logging]
S6[Input Validation]
end
POST -.-> S1
POST -.-> S2
POST -.-> S3
POST -.-> S4
POST -.-> S5
POST -.-> S6
PUT -.-> S1
PUT -.-> S2
PUT -.-> S3
PUT -.-> S6
PATCH -.-> S1
PATCH -.-> S2
PATCH -.-> S3
PATCH -.-> S6
DELETE -.-> S1
DELETE -.-> S2
DELETE -.-> S3
DELETE -.-> S4
DELETE -.-> S5
subgraph "Use Cases"
U1[POST: Sign up, Create order, Upload file]
U2[PUT: Update profile, Replace document]
U3[PATCH: Change email, Update status]
U4[DELETE: Remove account, Delete post]
end
style POST fill:#FFB6C1
style PUT fill:#FFD700
style PATCH fill:#FFA500
style DELETE fill:#FF6347
Security Notes
CRITICAL: POST, PUT, DELETE, PATCH are unsafe. Require proper authorization and CSRF protection.
Unsafe HTTP Methods:
- POST: Create new resources, often has side effects
- PUT: Replace entire resource
- DELETE: Remove resource
- PATCH: Partially update resource
- TRACE/CONNECT: Rarely used, potential security issues
Safety Implications:
- State modification: All unsafe methods modify server state
- Idempotency: DELETE and PUT are idempotent; POST is not
- Caching: Responses should not be cached
- CSRF vulnerable: Browser can trigger without user consent
- Authorization critical: Must verify permissions before execution
CSRF Protection:
- SameSite cookies: Mark cookies as SameSite=Strict/Lax
- CSRF tokens: Include tokens in state-changing requests
- Custom headers: Require custom headers for state changes
- Origin checking: Verify Origin header matches expected origin
- Referer checking: Verify Referer header (less reliable)
Request Body Security:
- Content-Type validation: Validate content type matches body
- Size limits: Limit request body size to prevent DoS
- Parsing safety: Safe parsing of JSON/XML
- Injection prevention: Sanitize input to prevent injections
- Type validation: Validate field types before processing
Monitoring & Logging:
- Log all mutations: Log all state-modifying operations
- Audit trail: Maintain audit trail of changes
- User context: Include user ID in mutation logs
- Timing: Record exact time of mutation
- Change tracking: Track before/after values for important fields
Best Practices
- Always require authentication: Unsafe methods should never be accessible without valid credentials
- Implement proper authorization: Check that the user has permission to perform the action
- Use idempotency keys for POST: Prevent duplicate resource creation from retries
- Return appropriate status codes: 201 for POST create, 200/204 for PUT/PATCH/DELETE
- Include Location header for POST: Point to the newly created resource
- Support partial updates with PATCH: Don’t require clients to send entire resource
- Make DELETE idempotent: Return 204 or 404 on retry, don’t error
- Validate input thoroughly: Check data types, formats, ranges, required fields
- Implement rate limiting: Prevent abuse of state-changing operations
- Log audit trails: Record who did what and when for accountability
Common Mistakes
No CSRF protection: State-changing operations vulnerable to cross-site request forgery attacks.
Missing authentication: Allowing unauthenticated POST, PUT, PATCH, DELETE requests.
Weak authorization: Checking if user is logged in but not if they own the resource they’re modifying.
No idempotency for POST: Creating duplicate resources when clients retry failed requests.
Using POST for everything: Not using PUT for updates or DELETE for removal - violates REST principles.
Not returning 201 for POST: Returning 200 instead of 201 Created with Location header.
Treating 404 on DELETE as error: DELETE is idempotent - 404 on retry is acceptable, not a failure.
No rate limiting: Allowing unlimited POST requests enables spam, abuse, and DoS attacks.
Exposing internal errors: Returning database errors or stack traces in 500 responses.
Allowing PATCH without validation: Accepting arbitrary JSON patches without checking allowed fields.
No audit logging: Not recording who created, updated, or deleted resources.
Ignoring data size limits: Accepting unlimited request body size for POST/PUT operations.