Your GET works. Your POST does not. The console mentions a preflight, and the network tab shows an OPTIONS request you never wrote. Nothing you change to the actual request makes any difference, because the actual request is never being sent.
Why a CORS Preflight Request Failed Before Your Code Ran
The browser asked permission first, and your server said no.
Before certain cross-origin requests, browsers send an OPTIONS request asking whether the real one is allowed. If that answer is unsatisfactory, the real request never happens.
Three things make a request non-simple and trigger this check:
- A method other than GET, HEAD or POST.
- Any header outside the small CORS safelist, including Authorization.
- A content type of application/json.
That last one explains why almost every modern API call gets preflighted. Sending JSON is enough on its own.
Our CORS tester sends the OPTIONS request separately, so you can see which step is failing.
Cause 1: The OPTIONS Request 404s
Your route only handles POST, so OPTIONS hits nothing.
Frameworks vary. Express with the cors package handles it for you; a hand-rolled router usually does not.
// Handle preflight explicitly
app.options('/api/*', cors());
Check the network tab for the status on the OPTIONS row. A 404 there is unambiguous and takes ten seconds to confirm.
Cause 2: An OPTIONS 401 Unauthorized
Your auth middleware is rejecting the preflight.
This one is genuinely counter-intuitive. A preflight deliberately carries no credentials — no cookies, no Authorization header, nothing.
So any middleware checking for a token sees an unauthenticated request and returns 401. The browser reads that as refusal and stops.
app.use((req, res, next) => {
if (req.method === 'OPTIONS') return next();
return requireAuth(req, res, next);
});
Skipping auth on OPTIONS is safe. A preflight cannot read data — it only asks a question.
Cause 3: The Access-Control-Allow-Methods Error
The method you want is not on the list.
The preflight response has to name every method your API accepts:
Access-Control-Allow-Methods: GET, POST, PUT, PATCH, DELETE, OPTIONS
We see this most on DELETE. It gets forgotten because nobody tests it until late, and framework defaults often cover only GET and POST.
Same applies to headers. Anything custom must appear in Access-Control-Allow-Headers, or the preflight refuses it.
Cause 4: A Redirect During Preflight
Browsers do not follow redirects on a preflight. At all.
A 301 or 302 aborts the whole thing, and the error message gives you no hint that a redirect was involved.
Two configurations cause it constantly: forcing https, and adding or removing a trailing slash. Both are invisible in your code.
The fix is to request the final URL directly. Inspect what your endpoint returns with our HTTP headers checker if you are not sure whether a redirect is in play.
Cause 5: Headers Present on One Response but Not the Other
The preflight and the real request are two separate responses.
Adding CORS headers to your OPTIONS handler only gets you past step one. The actual POST reply needs them too.
The reverse happens as well — headers on the route but nothing on the preflight. Either way you get a failure that looks intermittent because it depends which request you are watching.
In our experience this is the hardest of the five to spot, because the network tab shows a green OPTIONS row directly above a red one.
A Diagnosis Order That Saves Time
- Find the OPTIONS row in the network tab and read its status.
- 404? No route handler for OPTIONS.
- 401 or 403? Auth middleware is running on it.
- 301 or 302? A redirect is aborting it.
- 200 or 204 but still blocked? Read the response headers — something is missing from the allow lists.
That order resolves nearly every case we are asked about, usually in under five minutes.
Wrapping Up
When a CORS preflight request failed, the real request never left the browser. Start with the OPTIONS status code, because it points straight at the cause.
Exclude OPTIONS from authentication, list every method and header you accept, and make sure both responses carry the headers.
For the underlying mechanics, see how preflight requests actually work. If the origin header is the missing piece, the Access-Control-Allow-Origin fix covers that instead.
