Safe Methods

Fundamentals Security Notes Jan 9, 2026 HTTP
http rest http-methods read-only caching

Definition

Safe methods are HTTP methods that are defined to be read-only - they retrieve information without causing any side effects on the server. When you make a safe request, the server’s state remains unchanged. The four safe methods in HTTP are GET, HEAD, OPTIONS, and TRACE. These methods are guaranteed to be “safe to retry” because they don’t modify data, create resources, or trigger actions.

The key principle is that clients (browsers, apps, intermediaries like proxies and caches) can call safe methods freely without worrying about consequences. You can refresh a web page (sending GET requests) 100 times, and it won’t change anything on the server. This predictability allows aggressive caching, prefetching, and automated crawling.

It’s important to note that “safe” refers to the HTTP semantics, not implementation. A poorly designed API might use GET to delete data or send emails, violating the safety contract. But according to HTTP specification, safe methods should never alter server state. This contract is what makes the web scalable - caches and proxies can store and reuse responses from safe methods without permission.

Example

GET - Retrieve Resource: GET /api/products/123 fetches product data. Calling it once, ten times, or a million times doesn’t change the product. Browsers cache GET responses, CDNs store them, search engines crawl them - all because GET is safe.

HEAD - Check Resource Metadata: HEAD /api/files/photo.jpg returns only the headers (Content-Type, Content-Length, Last-Modified) without the file body. It’s used to check if a file exists or has been updated without downloading it. Safe to call repeatedly to monitor for changes.

OPTIONS - Discover Capabilities: OPTIONS /api/users returns allowed HTTP methods (GET, POST, PUT, DELETE) and CORS headers. Browsers use OPTIONS preflight requests automatically. It’s safe because it just describes what’s possible without doing anything.

TRACE - Diagnostic Loopback: TRACE /api/debug returns the request as received by the server, useful for debugging proxies and intermediaries. Rarely used in practice but defined as safe because it doesn’t modify state.

Web Browser Refreshing: When you hit F5 to refresh a webpage, the browser sends GET requests for the HTML, CSS, JavaScript, and images. This is safe - refreshing doesn’t submit forms, create accounts, or charge credit cards. The page looks the same after refresh.

Search Engine Crawling: Google’s crawler follows links and sends millions of GET requests daily. These must be safe - Google crawling your website shouldn’t delete data, trigger purchases, or send emails. That’s why crawlers only follow safe methods.

CDN Caching: Cloudflare caches GET responses from your API. When a user in Tokyo requests /api/products, Cloudflare serves the cached response from a nearby server. This only works because GET is safe - the cached response is guaranteed to be valid since the request won’t change server state.

Analogy

Reading a Book: When you read a book, you don’t change its contents. You can read the same page 100 times - it stays the same. GET is like reading - safe, non-destructive, repeatable.

Checking Your Bank Balance: Using an ATM to check your balance is a safe operation. You can check it 10 times in a row - your balance doesn’t change from checking. This is like HEAD or GET - retrieving information without modification.

Window Shopping: Walking past a store and looking in the window is safe. You’re not buying anything, you’re just looking. You can walk past the same window multiple times without consequence. Safe HTTP methods are like window shopping for data.

Library Catalog Search: Searching a library catalog to see if a book is available doesn’t reserve the book or check it out. You can search repeatedly without affecting availability. OPTIONS is like checking the catalog - it tells you what’s possible without doing it.

Museum Visiting: Walking through a museum and viewing exhibits is safe. The exhibits don’t change because you looked at them. You can visit the same museum many times, and the art remains the same. GET requests work this way - you’re observing, not modifying.

Code Example

# ===== GET - Retrieve Resource =====
# Most common safe method
GET /api/products/123 HTTP/1.1
Host: api.example.com
Accept: application/json

# Response
HTTP/1.1 200 OK
Content-Type: application/json
Cache-Control: max-age=3600
ETag: "v1-abc123"

{
  "id": 123,
  "name": "Widget",
  "price": 9.99,
  "stock": 100
}

# Can be called repeatedly without side effects
# Browsers, CDNs, proxies cache this aggressively

# ===== HEAD - Check Resource Metadata =====
# Returns only headers, no body
HEAD /api/files/photo.jpg HTTP/1.1
Host: api.example.com

# Response (no body)
HTTP/1.1 200 OK
Content-Type: image/jpeg
Content-Length: 524288
Last-Modified: Tue, 07 Jan 2026 14:30:00 GMT
ETag: "photo-v2-xyz789"

# Use case: Check if file was modified before downloading
# Safe to call many times to monitor for updates

# ===== OPTIONS - Discover Allowed Methods =====
# CORS preflight request
OPTIONS /api/users HTTP/1.1
Host: api.example.com
Origin: https://example.com
Access-Control-Request-Method: POST
Access-Control-Request-Headers: Content-Type, Authorization

# Response
HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://example.com
Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS
Access-Control-Allow-Headers: Content-Type, Authorization
Access-Control-Max-Age: 86400

# Safe - just discovers what's allowed, doesn't do anything

# ===== TRACE - Diagnostic Loopback =====
# Returns request as received (rarely used, often disabled)
TRACE /api/debug HTTP/1.1
Host: api.example.com
X-Custom-Header: test-value

# Response (echoes back the request)
HTTP/1.1 200 OK
Content-Type: message/http

TRACE /api/debug HTTP/1.1
Host: api.example.com
X-Custom-Header: test-value

# Safe - just echoes the request for debugging
// JavaScript examples of safe methods

// ===== GET - Fetching Data =====
// Can be called repeatedly without side effects
async function getProduct(id) {
  const response = await fetch(`/api/products/${id}`, {
    method: 'GET',
    headers: {
      'Accept': 'application/json'
    }
  });

  if (response.ok) {
    return await response.json();
  }
  throw new Error('Product not found');
}

// Safe to call multiple times
const product1 = await getProduct(123);
const product2 = await getProduct(123); // Same result
const product3 = await getProduct(123); // Same result

// Browsers cache GET responses automatically
fetch('/api/products/123', { method: 'GET' }); // Fetch from server
fetch('/api/products/123', { method: 'GET' }); // May use cached response

// ===== HEAD - Check if Resource Exists =====
async function fileExists(filename) {
  const response = await fetch(`/api/files/${filename}`, {
    method: 'HEAD'
  });

  return response.ok; // True if file exists, false otherwise
}

// Safe to check repeatedly
const exists = await fileExists('photo.jpg');
console.log('File exists:', exists);

// Check if file was modified
async function wasModified(filename, lastModified) {
  const response = await fetch(`/api/files/${filename}`, {
    method: 'HEAD'
  });

  const serverLastModified = response.headers.get('Last-Modified');
  return serverLastModified !== lastModified;
}

// ===== OPTIONS - CORS Preflight =====
// Browsers send this automatically for cross-origin requests
// You typically don't call it manually, but here's how:
async function checkCorsSupport(url) {
  const response = await fetch(url, {
    method: 'OPTIONS',
    headers: {
      'Access-Control-Request-Method': 'POST',
      'Access-Control-Request-Headers': 'Content-Type'
    }
  });

  const allowedMethods = response.headers.get('Access-Control-Allow-Methods');
  return allowedMethods.includes('POST');
}

// ===== SAFE vs UNSAFE Comparison =====
// Safe: Can be retried, cached, prefetched
async function getSafeData() {
  // GET is safe - no side effects
  const response = await fetch('/api/stats', { method: 'GET' });
  return response.json();
}

// Unsafe: Creates side effects
async function createOrder() {
  // POST is NOT safe - creates a new order
  const response = await fetch('/api/orders', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ product_id: 123, quantity: 1 })
  });
  return response.json();
}

// Safe method can be called in parallel without coordination
const [stats1, stats2, stats3] = await Promise.all([
  getSafeData(),
  getSafeData(),
  getSafeData()
]); // Safe - all three return same data

// Unsafe method should NOT be called in parallel (creates 3 orders!)
// DON'T DO THIS:
const badIdea = await Promise.all([
  createOrder(),
  createOrder(),
  createOrder()
]); // Creates 3 separate orders - NOT safe!

Diagram

graph TB
    subgraph "Safe Methods (Read-Only)"
        GET[GET
Retrieve resource] HEAD[HEAD
Get metadata only] OPTIONS[OPTIONS
Discover capabilities] TRACE[TRACE
Diagnostic loopback] end subgraph "Properties of Safe Methods" P1[No side effects] P2[Repeatable] P3[Cacheable] P4[Prefetchable] P5[Crawler-friendly] end GET --> P1 GET --> P2 GET --> P3 GET --> P4 GET --> P5 HEAD --> P1 HEAD --> P2 HEAD --> P3 OPTIONS --> P1 OPTIONS --> P2 TRACE --> P1 TRACE --> P2 subgraph "Common Use Cases" U1[Browser page load
Multiple GET requests] U2[CDN caching
Store GET responses] U3[File monitoring
HEAD to check updates] U4[CORS preflight
OPTIONS before POST] U5[Search engine crawling
GET all links] end GET -.-> U1 GET -.-> U2 GET -.-> U5 HEAD -.-> U3 OPTIONS -.-> U4 style GET fill:#90EE90 style HEAD fill:#90EE90 style OPTIONS fill:#90EE90 style TRACE fill:#90EE90

Security Notes

SECURITY NOTES

CRITICAL: GET, HEAD, OPTIONS should not modify state. Must be idempotent.

Safe Methods:

  • GET: Retrieve resource, no side effects
  • HEAD: Like GET but no response body
  • OPTIONS: Describe communication options
  • TRACE: Echo request (rarely used, security risk)

Idempotency:

  • Multiple calls: Same result as single call
  • No side effects: No state changes
  • Cacheable: Responses can be cached
  • Retryable: Safe to retry on failure

Security:

  • No state changes: Safe methods must not modify state
  • No CSRF: CSRF requires state-changing methods
  • Browser safe: Browsers can prefetch/retry safely
  • Caching safe: Safe to cache responses

Common Mistakes:

  • GET modifies state: Major security issue
  • Using POST for reads: Unnecessary, defeats caching
  • Sensitive data in URLs: Avoid even for safe methods
  • Unreliable idempotence: Cache inconsistency

Best Practices:

  • Use GET for reads: Standard, supports caching
  • Use HEAD for checks: Lightweight existence check
  • Document side effects: Never have hidden side effects
  • Cache appropriately: Set cache headers correctly

Best Practices

  1. Never modify state in GET requests: Follow HTTP semantics strictly - GET is read-only
  2. Use proper caching headers: Set Cache-Control, ETag, Last-Modified for GET responses
  3. Support HEAD for all GET endpoints: Return same headers as GET but without body
  4. Implement OPTIONS for CORS: Support preflight requests properly
  5. Make GET responses deterministic: Same request should return same data (until state changes externally)
  6. Use query parameters for filtering: /api/products?category=electronics&sort=price
  7. Return 304 Not Modified: When client sends If-None-Match or If-Modified-Since and resource hasn’t changed
  8. Document query parameters: Clearly specify supported filters, sorting, pagination
  9. Implement pagination for collections: Prevent returning huge datasets in single GET response
  10. Use appropriate status codes: 200 for success, 404 for not found, 304 for not modified

Common Mistakes

Using GET to delete or modify: Endpoints like /api/delete-user?id=123 violate HTTP semantics and are CSRF-vulnerable.

Including sensitive data in URLs: GET /api/reset-password?token=secret123 exposes tokens in logs and browser history.

Not caching GET responses: Missing Cache-Control headers wastes bandwidth and server resources.

Ignoring HEAD requests: Not supporting HEAD for endpoints that support GET.

Side effects in GET: Incrementing view counters, logging analytics, or sending notifications on GET requests.

Not implementing 304 Not Modified: Always returning full response even when client has cached version.

Treating OPTIONS as unsafe: Some frameworks block OPTIONS or require authentication, breaking CORS preflight.

Returning different data for same GET: Non-deterministic responses confuse caches and clients.

No pagination on collections: GET /api/products returning all 10,000 products instead of paginated results.

Using GET for sensitive operations: Password reset links that trigger action on GET instead of requiring POST confirmation.

Standards & RFCs