Skip to content

Simple vs Preflight Requests — Understanding the CORS Classification

DodaTech Updated 2026-06-28 4 min read

In this tutorial, you will learn about Simple vs Preflight Requests. We cover key concepts, practical examples, and best practices to help you master this topic.

Cross-origin requests fall into two categories: simple requests that proceed without a preflight, and preflight requests requiring an OPTIONS handshake, based on method, headers, and content-type criteria.

What You'll Learn

  • The three criteria for a simple request
  • Which methods, headers, and content types are allowed
  • Why the browser distinguishes between the two

Why It Matters

Choosing simple requests avoids an extra round-trip, speeding up your application. Understanding the distinction helps you design APIs that minimize preflight overhead.

flowchart LR
    A["Cross-Origin Request"] --> B{"Method is GET, HEAD, or POST?"}
    B -->|"No"| C["PREFLIGHT REQUIRED"]
    B -->|"Yes"| D{"Only simple headers?"}
    D -->|"No"| C
    D -->|"Yes"| E{"Content-Type is..."}
    E -->|"text/plain"| F["SIMPLE"]
    E -->|"application/x-www-form-urlencoded"| F
    E -->|"multipart/form-data"| F
    E -->|"application/json or others"| C
    style C fill:#fecaca,stroke:#dc2626
    style F fill:#86efac,stroke:#16a34a

Code Examples

// Simple request: GET with no custom headers
fetch('https://api.example.com/users')
  // No preflight needed

// Preflight request: POST with JSON
fetch('https://api.example.com/users', {
  method: 'POST',
  headers: {'Content-Type': 'application/json'},
  body: JSON.stringify({name: 'Alice'})
})
// Preflight needed because content-type is application/json

// Preflight request: DELETE method
fetch('https://api.example.com/users/1', {
  method: 'DELETE'
})
// Preflight needed because method is not GET/HEAD/POST
# Check if a request would be simple
SIMPLE_METHODS = {'GET', 'HEAD', 'POST'}
SIMPLE_CONTENT_TYPES = [
    'text/plain',
    'application/x-www-form-urlencoded',
    'multipart/form-data'
]
SIMPLE_HEADERS = {
    'accept', 'accept-language', 'content-language',
    'content-type'
}

def is_simple_request(method, headers):
    if method.upper() not in SIMPLE_METHODS:
        return False
    content_type = headers.get('Content-Type', '').lower()
    if content_type and content_type not in SIMPLE_CONTENT_TYPES:
        return False
    for header in headers:
        if header.lower() not in SIMPLE_HEADERS:
            return False
    return True
# Test if a request triggers preflight
# This DELETE request will always trigger preflight
curl -X DELETE https://api.example.com/resource/1 \
  -H "Origin: https://app.example.com" \
  -v 2>&1 | head -20

# This GET request is simple
curl -X GET https://api.example.com/resource \
  -H "Origin: https://app.example.com" \
  -v 2>&1 | head -20

Common Mistakes

1. Thinking All POST Requests Trigger Preflight

POST with content-type text/plain is simple and skips preflight.

2. Adding Custom Headers Accidentally

A single custom header like X-Requested-With triggers preflight.

3. Using PUT Instead of POST

PUT always triggers preflight. Use POST where possible to avoid it.

4. Assuming Same-Origin Requests Follow the Same Rules

Same-origin requests bypass all CORS checks. Preflight logic only applies cross-origin.

5. Forgetting That Fetch Defaults Change Behavior

fetch() defaults to same-origin mode but accepts cors mode. Using no-cors mode makes the response opaque.

Practice Questions

  1. What three HTTP methods qualify for simple requests?
  2. What content types are allowed in simple requests?
  3. Why do custom headers trigger preflight?
  4. Does adding a Content-Type header of application/json make a POST request preflighted?
  5. Can a browser make a simple request with credentials?

Answers:

  1. GET, HEAD, POST.
  2. text/plain, application/x-www-form-urlencoded, multipart/form-data.
  3. Custom headers may affect server behavior, so the server must explicitly allow them.
  4. Yes. application/json is not in the allowed list for simple requests.
  5. Yes, but with-with credentials, the server must respond with Access-Control-Allow-Credentials: true.

Challenge: For a given API endpoint, list all possible request combinations and classify each as simple or preflighted. Include variations of method, content-type, and custom headers.

FAQ

Why did the CORS specification create simple requests?

To allow legacy cross-origin form submissions and simple API calls to continue working without breaking existing web applications that relied on cross-origin form posts.

Can I force all requests to be preflighted?

Yes. Add a custom header like X-Custom-Header or use a non-simple method. This is sometimes useful for logging all cross-origin requests on the server.

Are HEAD requests always simple?

Yes. HEAD is a simple method as long as no non-simple headers or content types are used. Content-Type is irrelevant for HEAD requests since they have no body.

Does the browser send the Origin header with simple requests?

Yes. The Origin header is sent with all cross-origin requests, including simple ones. The server must still respond with appropriate CORS headers.

How does the no-cors mode in fetch affect simple requests?

In no-cors mode, the browser makes the request but makes the response opaque, meaning JavaScript cannot read any properties. Preflight is never sent in no-cors mode regardless of request type.

Mini Project

Create a test page with buttons for each request type: simple GET, POST with form data, POST with JSON, PUT, DELETE, and GET with custom header. Display whether each request triggered a preflight by inspecting the Network tab. Log the request type and the CORS headers received.

What's Next

Study the complete CORS headers reference to understand every Access-Control-* header, then deep dive into Access-Control-Allow-Origin behavior.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro