Safe methods — HTTP methods that must not change server state
Intermediate5 min read·eng-13-003
interview
Concept
Safe methods — HTTP methods that, by definition, MUST NOT cause any side effects on the server. A safe request is one the client can make without expecting any state change.
Safe methods: GET, HEAD, OPTIONS, TRACE. Unsafe methods: POST, PUT, PATCH, DELETE — these change server state.
What "safe" means precisely: The HTTP specification (RFC 9110) says safe methods are requests where "the client does not request, and does not expect, any state change on the origin server as a result of applying that method." This is a SEMANTIC contract, not a technical enforcement.
Important nuances:
- "Safe" doesn't mean the server can't log the request, update access counters, or do other non-observable operations. Those are incidental side effects.
- If you implement GET to delete records, it's technically possible but violates the safe-method contract. Crawlers and caches will break.
- Browsers prefetch links with GET — safe methods guarantee no accidental mutations from prefetching.
Safe vs idempotent comparison:
| Method | Safe? | Idempotent? |
|---|---|---|
| GET | Yes | Yes |
| HEAD | Yes | Yes |
| OPTIONS | Yes | Yes |
| PUT | No | Yes |
| DELETE | No | Yes |
| POST | No | No |
| PATCH | No | Depends |
Practical consequences:
- Caches (browsers, CDNs, proxies) only cache safe methods by default.
- Search engine crawlers only follow GET links — if you route a delete action to a GET URL, crawlers will delete your data.
- CSRF protection is often skipped for GET requests — they're expected to be safe.
- Laravel's
VerifyCsrfTokenmiddleware excludes GET, HEAD, OPTIONS by default.
Code Example
php
<?php
// ✅ SAFE — GET should ONLY read, never write
Route::get('/users/{id}', function (int $id) {
return response()->json(User::findOrFail($id));
// No state change — safe to call by caches, crawlers, prefetchers
});
// ❌ UNSAFE GET — this violates the safe-method contract
Route::get('/users/{id}/delete', function (int $id) {
User::findOrFail($id)->delete(); // WRONG! GET must not mutate state
return redirect('/users');
// A search engine crawling this URL will delete users!
});
// ✅ Correct — use DELETE method for deletion
Route::delete('/users/{id}', function (int $id) {
User::findOrFail($id)->delete();
return response()->noContent(); // 204
});
// HEAD — same as GET but returns headers only
// Used to check if a resource exists or get metadata without downloading the body
Route::get('/files/{id}', function (int $id) {
$file = File::findOrFail($id);
return response()
->file(storage_path("uploads/{$file->path}"))
->header('ETag', $file->hash)
->header('Content-Length', $file->size); // HEAD will return these headers only
});
// OPTIONS — describes what methods a resource supports
// Laravel/Symfony handle OPTIONS automatically for CORS preflight
// You can also add it manually:
Route::options('/users', function () {
return response('', 204)->header('Allow', 'GET, POST, OPTIONS');
});
// CSRF protection — Laravel skips for safe methods
// In VerifyCsrfToken.php, GET/HEAD/OPTIONS skip the CSRF check:
// protected function isReading(Request $request): bool
// {
// return in_array($request->method(), ['HEAD', 'GET', 'OPTIONS']);
// }
// Safe methods are cacheable — CDN caches GET responses automatically
// Unsafe methods invalidate caches
Route::get('/products', function () {
return response()
->json(Product::active()->get())
->header('Cache-Control', 'public, max-age=300'); // CDN can cache this!
});
Route::post('/products', function (CreateProductRequest $request) {
$product = Product::create($request->validated());
// POST response is NOT cached by default — unsafe method
return response()->json($product, 201);
});