• Uncategorised

Understanding CORS Preflight Requests: Why Your MCP Server Needs OPTIONS Support

f you’ve ever built a web API or MCP (Model Context Protocol) server, you’ve probably encountered mysterious OPTIONS requests appearing in your server logs. Or worse, you’ve seen CORS errors in the browser console blocking your perfectly valid requests. Today, we’ll demystify CORS preflight requests and show you why they’re essential for secure web applications.

What is a CORS Preflight Request?

A CORS (Cross-Origin Resource Sharing) preflight request is an automatic HTTP OPTIONS request that browsers send before making certain cross-origin requests. Think of it as the browser asking the server for permission before sending the actual request.

The Preflight Handshake

Here’s what happens behind the scenes:Step 1: Browser Sends Preflight (OPTIONS)

OPTIONS /api/endpoint HTTP/1.1

Origin: https://app.example.com

Access-Control-Request-Method: POST

Access-Control-Request-Headers: authorization, content-type

The browser is asking: “Can I send a POST request with Authorization and Content-Type headers to your server?”Step 2: Server Responds to Preflight

HTTP/1.1 200 OK

Access-Control-Allow-Origin: https://app.example.com

Access-Control-Allow-Methods: POST, GET, OPTIONS

Access-Control-Allow-Headers: Authorization, Content-Type

Access-Control-Max-Age: 86400

The server replies: “Yes, you can! And I’ll remember this for 24 hours so you don’t need to ask again.”Step 3: Browser Sends Actual RequestOnly after the preflight succeeds does the browser send your actual API request.

When Do Browsers Send Preflight Requests?

Not all requests trigger a preflight. Browsers only send OPTIONS requests when the request is “complex” according to CORS rules.

Triggers That Cause Preflight

1. Custom Headers (Most Common)Any header that’s not in the safe list triggers a preflight:

// This triggers preflight

fetch(‘http://api.example.com/data’, {

    headers: {

        ‘Authorization’: ‘Bearer token123’,  // ❌ Triggers preflight

        ‘X-API-Key’: ‘secret’,               // ❌ Triggers preflight

        ‘X-Custom-Header’: ‘value’           // ❌ Triggers preflight

    }

});

2. Non-Simple HTTP Methods

// These trigger preflight

fetch(url, { method: ‘PUT’ });     // ❌ Preflight

fetch(url, { method: ‘DELETE’ });  // ❌ Preflight

fetch(url, { method: ‘PATCH’ });   // ❌ Preflight

// These DON’T trigger preflight

fetch(url, { method: ‘GET’ });     // ✅ Simple

fetch(url, { method: ‘HEAD’ });    // ✅ Simple

fetch(url, { method: ‘POST’ });    // ✅ Simple (with simple headers)

3. Custom Content-Types

// Triggers preflight

fetch(url, {

    headers: {

        ‘Content-Type’: ‘application/json’  // Sometimes OK

    }

});

// Safe content types (no preflight):

‘Content-Type’: ‘text/plain’

‘Content-Type’: ‘application/x-www-form-urlencoded’

‘Content-Type’: ‘multipart/form-data’

Why MCP Servers Always Get Preflight Requests

If you’re building an MCP server, you’ll almost certainly encounter preflight requests. Here’s why:MCP clients typically use authentication headers:

const response = await fetch(‘http://localhost:8500/mcp/server’, {

    method: ‘POST’,

    headers: {

        ‘Content-Type’: ‘application/json’,

        ‘Authorization’: ‘Basic dXNlcjpwYXNz’,      // ⚠️ TRIGGERS PREFLIGHT!

        ‘Mcp-Protocol-Version’: ‘2024-11-05’

    },

    body: JSON.stringify({

        jsonrpc: ‘2.0’,

        method: ‘tools/call’,

        params: { name: ‘calculateSum’, arguments: { a: 5, b: 3 } }

    })

});

The Authorization header is not in the browser’s safe list, so a preflight is mandatory.

The Cost of Ignoring Preflight Requests

Without Proper OPTIONS Support:

Browser: OPTIONS /mcp/server

Server: 404 Not Found (or 405 Method Not Allowed)

Browser: ❌ CORS error! Blocking actual request.

JavaScript Console: 

  ❌ Access to fetch at ‘http://localhost:8500/mcp/server’ 

     has been blocked by CORS policy

Result: Your MCP client cannot connect. Your API is unusable from web browsers.

With Proper OPTIONS Support:

Browser: OPTIONS /mcp/server

Server: 200 OK + CORS headers

Browser: ✅ Preflight passed! Sending actual POST…

Server: 200 OK + {“result”: 8}

JavaScript: ✅ Success!

Result: Everything works smoothly.

Implementing Preflight Support: The Right Way

Here’s how to properly handle OPTIONS requests in your server:

1. Detect OPTIONS Requests

String method = request.getMethod();

if (“OPTIONS”.equals(method)) {

    // Handle preflight

}

2. Set Required CORS Headers

// Tell browser what methods are allowed

response.setHeader(“Access-Control-Allow-Methods”, “POST, GET, OPTIONS”);

// Tell browser what headers are allowed

response.setHeader(“Access-Control-Allow-Headers”, 

    “Content-Type, Accept, Authorization, X-API-Key”);

// Cache preflight response for 24 hours

response.setHeader(“Access-Control-Max-Age”, “86400”);

3. Set the Origin Header

String requestOrigin = request.getHeader(“Origin”);

if (isCorsAllowed(requestOrigin)) {

    response.setHeader(“Access-Control-Allow-Origin”, requestOrigin);

    response.setHeader(“Vary”, “Origin”);

}

4. Add Security Headers

response.setHeader(“X-Content-Type-Options”, “nosniff”);

response.setHeader(“X-Frame-Options”, “DENY”);

response.setHeader(“Referrer-Policy”, “strict-origin-when-cross-origin”);

5. Return 200 OK

response.setStatus(200);

// No body needed for preflight response

Advanced: Origin-Specific CORS Control

Instead of allowing all origins with Access-Control-Allow-Origin: *, implement fine-grained control:

// Configure allowed origins

List<String> allowedOrigins = Arrays.asList(

    “https://app.example.com”,

    “https://admin.example.com”,

    “*.example.com”  // Wildcard for subdomains

);

// Check if request origin is allowed

boolean isCorsAllowed(String origin) {

    if (allowedOrigins.contains(“*”)) {

        return true;  // Allow all

    }

    for (String allowed : allowedOrigins) {

        if (allowed.equals(origin)) {

            return true;  // Exact match

        }

        // Handle wildcard patterns like “*.example.com”

        if (allowed.startsWith(“*”)) {

            String pattern = allowed.substring(1);

            if (origin.endsWith(pattern)) {

                return true;

            }

        }

    }

    return false;

}

Benefits:

  • Restricts access to trusted domains only
  • Prevents unauthorized cross-origin requests
  • More secure than wildcard *
  • Supports flexible patterns like *.example.com

Performance Optimization: Preflight Caching

The Access-Control-Max-Age header tells browsers to cache the preflight response:

response.setHeader(“Access-Control-Max-Age”, “86400”);  // 24 hours

What this means:

  • First request: OPTIONS + POST (2 requests)
  • Subsequent requests for 24 hours: POST only (1 request)
  • 50% reduction in requests for authenticated clients

Why CORS Preflight Exists: Security

Preflight requests are a security feature, not a bug. They protect users from malicious websites.

Without Preflight Protection:

  1. You log into bank.com
  2. You visit evil.com
  3. evil.com sends authenticated requests to bank.com using your cookies
  4. ❌ Your money is transferred to attacker

With Preflight Protection:

  1. You log into bank.com
  2. You visit evil.com
  3. Browser sends OPTIONS to bank.com
  4. bank.com rejects evil.com origin
  5. ✅ Browser blocks the actual request – your money is safe

Common Mistakes to Avoid

Mistake 1: Returning 405 for OPTIONS

// ❌ Wrong

if (!”POST”.equals(method)) {

    response.sendError(405, “Method Not Allowed”);

}

This blocks preflight requests. Always allow OPTIONS.

Mistake 2: Not Including Required Headers

// ❌ Incomplete

response.setHeader(“Access-Control-Allow-Origin”, “*”);

// Missing Allow-Methods and Allow-Headers!

Preflight will fail without these headers.

Mistake 3: Checking Auth on OPTIONS Requests

// ❌ Wrong

if (“OPTIONS”.equals(method)) {

    if (!isAuthenticated(request)) {  // Don’t check auth on preflight!

        return unauthorized();

    }

}

Preflight requests don’t include auth tokens. Check auth only on actual requests.

Mistake 4: Validating Content-Type on OPTIONS

// ❌ Wrong

if (!”application/json”.equals(contentType)) {

    return badRequest();

}

OPTIONS requests have no body, so Content-Type validation should be skipped.

Debugging CORS Issues

Check Browser DevTools

Open Network tab and look for:

  • OPTIONS request (preflight) – should return 200
  • POST request (actual) – should include your data

Common Error Messages

“has been blocked by CORS policy: Response to preflight request doesn’t pass access control check”

  • Fix: Add missing Access-Control-Allow-Headers

“Method PUT is not allowed by Access-Control-Allow-Methods”

  • Fix: Add PUT to Access-Control-Allow-Methods

“The ‘Access-Control-Allow-Origin’ header contains multiple values”

  • Fix: Set origin header only once, not in multiple places

Testing Your Preflight Implementation

Use curl to test OPTIONS requests:

curl -X OPTIONS http://localhost:8500/api/endpoint \

  -H “Origin: https://app.example.com” \

  -H “Access-Control-Request-Method: POST” \

  -H “Access-Control-Request-Headers: authorization, content-type” \

  -v

Look for these headers in the response:

HTTP/1.1 200 OK

Access-Control-Allow-Origin: https://app.example.com

Access-Control-Allow-Methods: POST, GET, OPTIONS

Access-Control-Allow-Headers: authorization, content-type

Access-Control-Max-Age: 86400

Conclusion

CORS preflight requests are an essential part of web security. While they might seem like an annoyance at first, they protect users from cross-origin attacks while enabling legitimate cross-origin communication.Key Takeaways:✅ Always handle OPTIONS requests in your API✅ Set all required CORS headers (Allow-Methods, Allow-Headers, Max-Age)✅ Use origin-specific CORS for better security✅ Cache preflight responses with Max-Age for better performance✅ Never check authentication on preflight requestsBy properly implementing preflight support, you ensure your MCP servers and APIs work seamlessly with web-based clients while maintaining strong security.


About the AuthorThis article is based on real-world implementation experience building MCP servers with ColdFusion and follows best practices from the CORS specification and modern API design patterns.Further Reading:

  • MDN Web Docs: CORS
  • W3C CORS Specification
  • OWASP CORS Security Cheat Sheet

You can now copy this entire text and paste it directly into WordPress’s visual editor. It will format correctly with headers, code blocks, and lists!

You may also like...