Reputation: 51
Situation:
I have an Express\Node server using the CORs middleware module and a separate React application generated with Create-React-App. The React app makes API calls to the Node server to perform simple CRUD operations. Pretty straight forward stuff and works as expected when running locally. I even get CORs errors locally if I remove the CORs middleware. I'm running Node locally with 'nodemon src/server.js' and React with 'react-scripts start'.
Node is running on port 8000
React is running on port 3000
PROBLEM:
Once deploying both the Node server and React app to production, which is an Ubuntu server with NGINX, I get a CORs error when calling the Node API from the React app
Cross-Origin Request Blocked: The Same Origin Policy disallows reading the remote resource at http://localhost:8000/api/auth/login. (Reason: CORS request did not succeed)
The production Node server is running with PM2 and works as expected when calling it from Postman. The PM2 evosystem.config.js file looks like this
module.exports = {
apps : [
{
name: "commonapi",
script: "./src/server.js",
env: {
"PORT": 8000,
"NODE_ENV": "production",
},
}
]
};
For the production React app, I've done a production build and moved the static files to an NGINX in /var/www/mysite. The NGINX config for the React app server block started like this:
server {
listen 80;
listen [::]:80;
root /var/www/mysite/build;
index index.html index.htm;
server_name mysite.com www.mysite.com;
location / {
try_files $uri $uri/ /index.html;
}
}
The login page is rendered as expected. The CORs error is thrown when the React front-end makes a call to the Node API at localhost:8000 to authenticate the user.
I tried to address this issue by adding Access Control headers to my server block in the NGINX config but unfortunately, this has not worked.
server {
listen 80;
listen [::]:80;
root /var/www/mysite/build;
index index.html index.htm;
server_name mysite.com www.mysite.com;
location / {
if ($request_method = 'OPTIONS') {
add_header 'Access-Control-Allow-Origin' '*';
add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
#
# Custom headers and headers various browsers *should* be OK with but aren't
#
add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified- Since,Cache-Control,Content-Type,Range';
#
# Tell the client that this pre-flight info is valid for 20 days
#
add_header 'Access-Control-Max-Age' 1728000;
add_header 'Content-Type' 'text/plain; charset=utf-8';
add_header 'Content-Length' 0;
return 204;
}
if ($request_method = 'POST') {
add_header 'Access-Control-Allow-Origin' '*';
add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range';
add_header 'Access-Control-Expose-Headers' 'Content-Length,Content-Range';
}
if ($request_method = 'GET') {
add_header 'Access-Control-Allow-Origin' '*';
add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range';
add_header 'Access-Control-Expose-Headers' 'Content-Length,Content-Range';
}
try_files $uri $uri/ /index.html;
}
}
Partial Node Server Code:
require('dotenv').config();
const express = require('express');
const morgan = require('morgan');
const cors = require('cors');
const helmet = require('helmet');
const { NODE_ENV } = require('./config');
const ResearchRouter = require('./research/research-router');
const usersRouter = require('./users/users-router');
const authRouter = require('./auth/auth-router');
const app = express();
const morganOption = (NODE_ENV === 'production')
? 'tiny'
: 'common';
app.use(morgan(morganOption));
app.use(helmet());
app.use(cors({
origin : '*',
"methods": "GET,HEAD,PUT,PATCH,POST,DELETE",
"preflightContinue": false,
"optionsSuccessStatus": 204
}));
app.use('/api/auth', authRouter);
app.use('/api/register', usersRouter);
app.use('/api/research', ResearchRouter);
app.use(function errorHandler(error, req, res, next) {
let response
if (NODE_ENV === 'production') {
response = {error: {message: 'server error'}}
} else {
console.error(error)
response = {message: error.message, error}
}
res.status(500).json(response)
})
module.exports = app;
Upvotes: 2
Views: 4201
Reputation: 51
I was able to fix this issue and ultimately the problem was with the front-end environment variables and the NGINX config. I also added certs from LetsEncrypt, which was not part of the problem but needed to be done.
For the front-end:
The API location in the environment variable file .env.production was changed from
'REACT_APP_API_ENDPOINT=http://localhost:8000'
to
REACT_APP_API_ENDPOINT='https://example.com'
Then I rebuilt the project with react-scripts build and deployed(copyed) the static files to NGINX's /var/www/ folder.
The NGINX config was updated as follows:
server {
## This server block is listening on port 443 and
## encrypts all connection with SSL certs from LetsEncrypt
listen [::]:443 ssl ipv6only=on; # managed by Certbot
listen 443 ssl; # managed by Certbot
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem; # managed by Certbot
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem; # managed by Certbot
include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot
## Points to Create-React-App production build static files
root /var/www/example.com/build;
index index.html index.htm;
server_name example.com www.example.com;
## Requests from the browser for React app resources are handled here
location / {
try_files $uri $uri/ /index.html;
}
## Api call from the React app to the Node server are handled here
location /api {
proxy_pass http://localhost:8000; #or whatever port your node server runs on
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_cache_bypass $http_upgrade;
}
}
server {
## This server block handles requests coming in on port 80
## and redirects them to port 443 in the server block above
## so the connections can be encrypted
listen 80;
listen [::]:80;
if ($host = www.example.com) {
return 301 https://$host$request_uri;
} # managed by Certbot
if ($host = example.com) { return 301 https://$host$request_uri;
} # managed by Certbot
server_name example.com www.example.com;
return 404; # managed by Certbot
}
Upvotes: 3