CORS Explained: Cross-Origin Resource Sharing Guide 2025
Why the browser blocks cross-origin requests, what preflight is, and how to set headers so your API works from the frontend.
If you've built web applications, you've likely encountered the dreaded CORS error: Access to fetch at 'https://api.example.com' from origin 'https://myapp.com' has been blocked by CORS policy. CORS (Cross-Origin Resource Sharing) is a security mechanism that browsers enforce, but it's often misunderstood. Let's demystify CORS and learn how to handle it correctly.
What is CORS?
CORS is a browser security feature that restricts web pages from making requests to a different domain than the one that served the web page. This is part of the Same-Origin Policy, which prevents malicious websites from accessing sensitive data from other sites.
An origin consists of three parts:
- Protocol: http:// or https://
- Domain: example.com
- Port: 80, 443, 3000, etc. (default ports are implicit)
Two URLs have the same origin only if all three parts match. For example:
https://example.com/page1 → Same origin
https://example.com/page2 → Same origin
https://api.example.com → Different origin (different subdomain)
http://example.com → Different origin (different protocol)
https://example.com:8080 → Different origin (different port)Why Does CORS Exist?
Without CORS, a malicious website could:
- Make authenticated requests to your bank's API using cookies stored in your browser
- Read responses from APIs that contain sensitive data
- Perform actions on your behalf without your knowledge
CORS allows servers to explicitly permit cross-origin requests while maintaining security. It's an opt-in system—servers must explicitly allow cross-origin requests.
How CORS Works
When a browser makes a cross-origin request, it follows this process:
1. Simple Requests
For "simple" requests (GET, POST with certain content types), the browser makes the request directly and checks the response headers:
- If
Access-Control-Allow-Originheader matches the request origin, the request succeeds - If the header is missing or doesn't match, the browser blocks the response
2. Preflight Requests
For "non-simple" requests (PUT, DELETE, custom headers, JSON content type), the browser first sends an OPTIONS request (preflight) to check if the actual request is allowed:
// Browser sends preflight request
OPTIONS /api/users HTTP/1.1
Origin: https://myapp.com
Access-Control-Request-Method: POST
Access-Control-Request-Headers: Content-Type
// Server responds with allowed methods/headers
HTTP/1.1 200 OK
Access-Control-Allow-Origin: https://myapp.com
Access-Control-Allow-Methods: POST, GET, OPTIONS
Access-Control-Allow-Headers: Content-Type
Access-Control-Max-Age: 86400
// If preflight succeeds, browser makes actual request
POST /api/users HTTP/1.1
Origin: https://myapp.com
Content-Type: application/jsonCORS Headers Explained
Access-Control-Allow-Origin
Specifies which origins are allowed to access the resource. Can be a specific origin or * for all origins (not allowed with credentials).
Access-Control-Allow-Origin: https://myapp.com
// or
Access-Control-Allow-Origin: *Access-Control-Allow-Methods
Specifies which HTTP methods are allowed for cross-origin requests. Used in preflight responses.
Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONSAccess-Control-Allow-Headers
Specifies which headers can be used in the actual request. Used in preflight responses.
Access-Control-Allow-Headers: Content-Type, Authorization, X-Requested-WithAccess-Control-Allow-Credentials
When set to true, allows cookies and authorization headers to be sent with cross-origin requests. When using credentials, Access-Control-Allow-Origin cannot be *.
Access-Control-Allow-Credentials: trueAccess-Control-Max-Age
Specifies how long (in seconds) the preflight response can be cached. Reduces the number of preflight requests.
Access-Control-Max-Age: 86400 // 24 hoursCommon CORS Errors and Solutions
Error: "No 'Access-Control-Allow-Origin' header"
The server isn't sending the required CORS headers. Solution: Add CORS headers to your server response.
Error: "Credentials flag is true, but 'Access-Control-Allow-Credentials' is not 'true'"
You're sending credentials (cookies, auth headers) but the server hasn't explicitly allowed them. Solution: Set Access-Control-Allow-Credentials: true on the server.
Error: "Method PUT is not allowed by Access-Control-Allow-Methods"
The server's preflight response doesn't include the HTTP method you're trying to use. Solution: Add the method to Access-Control-Allow-Methods.
Error: "Request header field Authorization is not allowed"
The server's preflight response doesn't allow the custom header you're sending. Solution: Add the header to Access-Control-Allow-Headers.
Implementing CORS on the Server
Express.js with cors Middleware
const express = require('express');
const cors = require('cors');
const app = express();
// Allow all origins (development only)
app.use(cors());
// Allow specific origins
app.use(cors({
origin: 'https://myapp.com',
credentials: true,
methods: ['GET', 'POST', 'PUT', 'DELETE'],
allowedHeaders: ['Content-Type', 'Authorization']
}));
// Allow multiple origins
const allowedOrigins = [
'https://myapp.com',
'https://www.myapp.com',
'http://localhost:3000'
];
app.use(cors({
origin: function (origin, callback) {
if (!origin || allowedOrigins.indexOf(origin) !== -1) {
callback(null, true);
} else {
callback(new Error('Not allowed by CORS'));
}
},
credentials: true
}));Manual CORS Headers (Any Framework)
// Handle preflight requests
app.options('/api/*', (req, res) => {
res.header('Access-Control-Allow-Origin', 'https://myapp.com');
res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
res.header('Access-Control-Allow-Headers', 'Content-Type, Authorization');
res.header('Access-Control-Allow-Credentials', 'true');
res.sendStatus(200);
});
// Handle actual requests
app.use('/api', (req, res, next) => {
res.header('Access-Control-Allow-Origin', 'https://myapp.com');
res.header('Access-Control-Allow-Credentials', 'true');
next();
});Next.js API Routes
// pages/api/users.ts
import { NextApiRequest, NextApiResponse } from 'next';
export default function handler(
req: NextApiRequest,
res: NextApiResponse
) {
// Set CORS headers
res.setHeader('Access-Control-Allow-Origin', 'https://myapp.com');
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
res.setHeader('Access-Control-Allow-Credentials', 'true');
// Handle preflight
if (req.method === 'OPTIONS') {
return res.status(200).end();
}
// Handle actual request
// ... your API logic
}CORS Security Best Practices
1. Never Use * in Production
While Access-Control-Allow-Origin: * is convenient for development, it allows any website to make requests to your API. Always specify exact origins in production.
2. Validate Origins Dynamically
Don't hardcode origins. Use environment variables or a database to manage allowed origins. This makes it easier to add/remove origins without code changes.
3. Use Credentials Carefully
Only enable Access-Control-Allow-Credentials when you actually need to send cookies or auth headers. This reduces the attack surface.
4. Limit Allowed Methods and Headers
Only allow the HTTP methods and headers your API actually uses. Don't use wildcards like Access-Control-Allow-Methods: *.
5. Cache Preflight Responses
Use Access-Control-Max-Age to cache preflight responses. This reduces the number of OPTIONS requests and improves performance.
CORS vs Other Solutions
CORS vs JSONP
JSONP (JSON with Padding) was an older workaround for cross-origin requests. It only works for GET requests and has security vulnerabilities. CORS is the modern, secure solution.
CORS vs Proxy
You can avoid CORS entirely by proxying requests through your own server. However, this adds latency and server load. CORS is the preferred solution for direct client-to-API communication.
Debugging CORS Issues
When debugging CORS issues:
- Check browser console: CORS errors are clearly logged with details
- Inspect network tab: Look for preflight OPTIONS requests and their responses
- Verify headers: Ensure all required CORS headers are present and correct
- Test with curl: Use curl to verify server responses without browser CORS enforcement
- Check credentials: If using credentials, ensure
Access-Control-Allow-Credentialsis set
Conclusion
CORS is a critical security mechanism that protects users from malicious websites. While it can be frustrating when first encountered, understanding how it works makes it much easier to implement correctly.
Remember: CORS is enforced by browsers, not servers. Server-side code doesn't need CORS—it's only needed when browsers make cross-origin requests. Always configure CORS headers on your API servers, validate origins carefully, and never use wildcards in production.
For testing API requests and inspecting CORS headers, check out our API Tester tool, which helps you debug CORS issues in real-time.
Part of the ThenCatch blog. Learn more about us or browse more guides.