You call an API from the browser. The URL is fine, the parameters are correct, but the console throws a red error:
Access to fetch at 'https://api.example.com/data' from origin 'http://localhost:3000'
has been blocked by CORS policy: Response to preflight request doesn't pass
access control check: No 'Access-Control-Allow-Origin' header is present.
What's more confusing: you open the Network panel and find an OPTIONS request you never explicitly sent. That OPTIONS request is the so-called CORS preflight request.
When Does a Preflight Request Get Triggered?
The browser's same-origin policy dictates that a page can only request resources with the same protocol, domain, and port. CORS (Cross-Origin Resource Sharing) is the server-side mechanism that "authorizes" cross-origin access. A preflight request is the browser asking the server — before sending a "potentially side-effecting" request — "Can I make this kind of request to you?"
Simple requests do not trigger a preflight. They must meet all of these:
- The method is
GET,HEAD, orPOST - No custom request headers (only safe headers like
Accept,Accept-Language,Content-Language,Content-Type) Content-Typeis onlytext/plain,multipart/form-data, orapplication/x-www-form-urlencoded
Any of the following triggers a preflight:
- Using non-simple methods like
PUT,DELETE,PATCH - Adding custom request headers, e.g.
Authorization: Bearer xxx - Setting
Content-Typetoapplication/json(the most common trap for frontend devs)
// This triggers a preflight because Content-Type is application/json
fetch('https://api.example.com/users', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name: 'test' })
});
// This one too, because it uses DELETE
fetch('https://api.example.com/users/1', {
method: 'DELETE'
});
// And this one, because of the custom header
fetch('https://api.example.com/data', {
headers: { 'X-API-Key': 'abc123' }
});
What Does the Preflight Actually Send? How Do You Read the Response?
When a preflight is triggered, the browser automatically sends an OPTIONS request with these key headers:
OPTIONS /users HTTP/1.1
Host: api.example.com
Origin: http://localhost:3000
Access-Control-Request-Method: POST
Access-Control-Request-Headers: Content-Type, Authorization
Origin: the origin making the request (your frontend address)Access-Control-Request-Method: the method the actual request will useAccess-Control-Request-Headers: the custom headers the actual request will send
The server must "approve" these conditions in its response:
HTTP/1.1 204 No Content
Access-Control-Allow-Origin: http://localhost:3000
Access-Control-Allow-Methods: GET, POST, PUT, DELETE
Access-Control-Allow-Headers: Content-Type, Authorization
Access-Control-Max-Age: 86400
What each response header means:
| Response Header | Purpose |
|--------|------|
| Access-Control-Allow-Origin | Which origins are allowed. * means all origins (but cannot be used with credentials) |
| Access-Control-Allow-Methods | The list of allowed HTTP methods |
| Access-Control-Allow-Headers | The allowed custom request headers |
| Access-Control-Max-Age | How long (seconds) to cache the preflight result, to avoid sending OPTIONS on every request |
| Access-Control-Allow-Credentials | Whether cookies can be sent (when set to true, Origin cannot be *) |
If the server's response headers don't satisfy the browser's checks, the actual request is never sent — and you see that CORS error.
Common CORS Errors and How to Fix Them
Error 1: No 'Access-Control-Allow-Origin' header
The server has no CORS configuration at all. Add the response headers on the server:
// Express example
app.use((req, res, next) => {
res.header('Access-Control-Allow-Origin', 'http://localhost:3000');
res.header('Access-Control-Allow-Methods', 'GET,POST,PUT,DELETE');
res.header('Access-Control-Allow-Headers', 'Content-Type,Authorization');
next();
});
// Or just use the cors middleware
const cors = require('cors');
app.use(cors({ origin: 'http://localhost:3000' }));
Error 2: Method not allowed in preflight
The server's CORS config doesn't include the HTTP method you're using. For example, you send PUT but Access-Control-Allow-Methods only lists GET, POST.
Error 3: Request header field X is not allowed
A custom request header wasn't approved by Access-Control-Allow-Headers. Check that every non-standard header you send is in the server's allow list.
Error 4: credentials mode is 'include' but Allow-Origin is '*'
When fetch is configured with credentials: 'include' (to send cookies), the server cannot use Access-Control-Allow-Origin: *. It must specify the exact Origin and add Access-Control-Allow-Credentials: true.
Why Local API Testing Tools Aren't Subject to CORS
Here's a key insight: CORS is a browser security mechanism, not an HTTP protocol restriction.
fetchandXMLHttpRequestin the browser are subject to the same-origin policy- Requests made by CLI tools (curl, httpie), desktop apps, and backend services are completely unaffected by CORS
This is why hitting an endpoint with Postman or curl works fine, but switching to the browser gives you a CORS error.
For frontend developers who hit CORS issues constantly during development, a practical approach is to use an API testing tool that doesn't go through the browser. The API Tester makes HTTP requests directly from your local machine, bypassing the browser sandbox entirely, so it never triggers preflight requests or CORS checks. You can focus on whether the endpoint's logic is correct without being distracted by cross-origin configuration.
Of course, in production you still need to configure CORS correctly — because end users access your site through a browser. But during development and debugging, separating "endpoint logic issues" from "CORS configuration issues" makes troubleshooting far more efficient.
Temporary Workarounds During Development
If you don't have permission to change the server's CORS config, during development you can:
- Frontend proxy: Configure a proxy in
vite.config.jsorwebpack devServerso the dev server forwards requests on your behalf - Browser extension: Temporarily disable CORS checks (dev environment only — never use for daily browsing)
- Local API tool: Use the API Tester to hit the endpoint directly, bypassing browser restrictions
// Vite proxy config example
export default {
server: {
proxy: {
'/api': {
target: 'https://api.example.com',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, '')
}
}
}
}
The core of understanding CORS preflight in one sentence: before sending a "non-simple request," the browser sends an OPTIONS request to ask the server's permission, and the server responds via the Access-Control-* series of headers. Once you understand the trigger conditions and how the response headers map to them, the vast majority of CORS issues can be located and fixed in minutes.
Next time you hit a CORS error, open the Network panel, find that OPTIONS request, and compare the Access-Control-Request-* values with the Access-Control-Allow-* values — the answer is usually right there. If you just want to confirm the endpoint itself works, fire a request with the API Tester — local processing, no CORS headaches, and no worry about request data passing through a third-party server.
相关文章
Online JWT Generator/Decoder: JSON Web Token Tool
Learn JWT (JSON Web Token) structure, how it works, and use cases. Understand how to use an online tool to generate and decode JWTs, plus security best practices.
Online Certificate Decoder: Parse SSL/TLS Certificate Information
Learn how to use an online tool to decode and view SSL/TLS certificate details. Understand certificate structure, common fields, and how to verify certificate validity.
Complete Guide to HTTP API Testing: From Basics to Advanced Debugging Techniques
Master the core skills of HTTP API testing, including GET/POST/PUT/DELETE requests, header configuration, request body formats, authentication methods, and debugging techniques. Boost your development efficiency with an online API testing tool.