CORS: Why do I get successful preflight OPTIONS, but still get CORS error with post?

Viewed 1458

I have a Vue front end using axios to make http requests, and a Node back end using express. They are on different domains (when running locally, BE port is 3080 and FE port is 3000), and I am using the cors library in the Node backend to deal with CORS issues. In spite of preflight working, the post request still fails with a CORS error!

When I send a post request, the browser gets a beautiful 204 successful response for the preflight request, complete with these beautiful successful headers:

Access-Control-Allow-Headers: content-type
Access-Control-Allow-Methods: POST
Access-Control-Allow-Origin: http://localhost:3000
Connection: keep-alive
Content-Length: 0
Date: Thu, 21 Apr 2022 03:25:54 GMT
Keep-Alive: timeout=5
Vary: Origin, Access-Control-Request-Headers
X-Powered-By: Express

So why does my POST request still fail with a CORS error?

Don't know if this is relevant, but it might be: I have tried this in both Brave (chromium based) and Firefox. Both of them have the same basic result, but with one difference: in Brave dev tools, it seems to show that the POST request was sent first, and then the preflight OPTIONS request. That would seem like an obvious red flag, BUT... in Firefox the dev tools show the preflight OPTIONS was sent first, followed by the POST. But as I said, the preflight succeeds and the post fails with a CORS error in both browsers.

CORS Error Screenshot (Firefox): enter image description here

Back End Code

app.js:

const express = require('express');
const cors = require('cors');
const app = express();
const port = 3080;

app.use(express.urlencoded({ extended: false }));
app.use(express.json());
app.use(cors());
app.options('/signup', cors());

app.post('/signup', (req, res) => {
  res.status(200).json({ message: 'SUCCESS'}).send();
})

app.listen(port, () => {
  console.log(`Listening on port ${port}`)
})

Front End Code

apiClient.js:

import axios from "axios";

const apiClient = axios.create({
  baseURL: "http://localhost:3080",
  headers: {
    "Content-type": "application/json"
  }
});

export default apiClient;

authService.js:

import apiClient from '../../../../common/utils/apiClient';

export const login = async (email, password) => {
  return await apiClient.get(`/login?email=${email}&password=${password}`);
}

export const signup = async (user) => {
  return await apiClient.post('/signup', user, {
    headers: {
      'Content-Type': 'application/json'
    }
  });
}

Script in SignUp.vue:

<script setup>
import { signup } from '../service/authService'
async function handleSignup(email, password) {
    await signup(email, password);
}
</script>
2 Answers

Here's the problem. The root issue actually has nothing to do with CORS, but rather a format error. Due to weird quirks in how CORS works, these kind of errors show up as CORS issues.

The formatting error is due to a string value being sent in the request body instead of a json object like the backend is expecting. You can see this in SignUp.vue, where we're passing an email and password instead of a single user object:

await signup(email, password);

When looking at the actual error response message in the network tab, we see the issue is SyntaxError: Unexpected token " in JSON at position 0: enter image description here

The fix is simple, put braces around the email and object:

await signup({ email, password });

Moral of the story, always look at your actual error message, don't assume it's CORS just because the network tab in dev tools is lying to you about it in the "Transferred" column!

Cross-Origin Resource Sharing (CORS) is an HTTP-header based mechanism that allows a server to indicate any origins (domain, scheme, or port) other than its own from which a browser should permit loading resources. CORS also relies on a mechanism by which browsers make a "preflight" request to the server hosting the cross-origin resource, in order to check that the server will permit the actual request. In that preflight, the browser sends headers that indicate the HTTP method and headers that will be used in the actual request. When you tried to hit a any post request from any server host to another host it required a validtion of origin access whether you have a api permission or not. That's why you need to pass a access control origin's in header while making a post request or you can provide the access token in parameter of request. I hope this can help you.

await apiClient.post('/signup', user, {
    headers: {
        'Content-Type': 'application/json',
        'Access-Control-Allow-Origin': '*',
        'Access-Control-Allow-Methods': '*',
        'Access-Control-Allow-Headers': '*'
    }
});
Related