2

I'm trying to deploy a full-stack React/Node.js web app with Letsencrypt to production on an Ubuntu 20.04 LTS server. I've built the client and the web page is rendering over https with no problem. The issue arises when I try to make a POST request to the backend.

The React client is running on example.com:3000.
The Node.js server is running on example.com:9000.

When I trigger a call to the backend, e.g. example.com:9000/signIn to get the user's credentials and sign them in, I get 2 errors in my browser console:

POST https://example.com:9000/signIn net::ERR_SSL_PROTOCOL_ERROR coming from one of my React components as well as this error: Uncaught (in promise) TypeError: Failed to fetch.

When I tail the nginx logs, all I see are GET requests loading my front end files/content. Also when I run my node.js server, all I see are logs I left in the application to show that the database is connected successfully. I'm expecting to see some logs indicating whether the user was authenticated or not.

Nginx configuration in /etc/nginx/sites-enabled/example.com:

server {
         root /home/ubuntu/apps/mysite/client/build;

        # Add index.php to the list if you are using PHP
        index index.html index.htm index.nginx-debian.html;

        server_name example.com www.example.com;

        location / {
                try_files $uri /index.html;
        }

         location /server {
            proxy_pass https://localhost:9000;
            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;
        }


    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


}
server {
    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


        listen 80;
        listen [::]:80;

        server_name example.com www.example.com;
    return 404; # managed by Certbot
}

package.json in /server:

{
  "name": "server",
  "version": "0.0.0",
  "private": true,
  "scripts": {
    "start": "node ./bin/www"
  },
  "dependencies": {
    "bcrypt": "^4.0.1",
    "bcryptjs": "^2.4.3",
    "constantinople": "^4.0.1",
    "cookie-parser": "~1.4.4",
    "cookie-session": "^1.4.0",
    "cors": "^2.8.5",
    "debug": "~2.6.9",
    "dotenv": "^8.2.0",
    "express": "~4.16.1",
    "express-session": "^1.17.1",
    "http-errors": "~1.6.3",
    "jade": "~1.11.0",
    "morgan": "~1.9.1",
    "mysql": "^2.18.1",
    "nodemailer": "^6.4.17",
    "passport": "^0.4.1",
    "passport-http-bearer": "^1.0.1",
    "passport-local": "^1.0.0"
  }
}

package.json in /client:

{
  "name": "client",
  "version": "0.1.0",
  "private": true,
  "dependencies": {
    "@testing-library/jest-dom": "^4.2.4",
    "@testing-library/react": "^9.5.0",
    "@testing-library/user-event": "^7.2.1",
    "bootstrap": "^4.4.1",
    "chart.js": "^2.9.3",
    "cors": "^2.8.5",
    "d3": "^6.2.0",
    "moment": "^2.29.1",
    "morris.js.so": "^0.5.1",
    "node-sass": "^4.14.1",
    "perm": "^1.0.0",
    "react": "^16.13.1",
    "react-bootstrap": "^1.0.1",
    "react-chartkick": "^0.4.1",
    "react-dom": "^16.13.1",
    "react-facebook-login": "^4.1.1",
    "react-feather": "^2.0.4",
    "react-google-login": "^5.1.10",
    "react-router-dom": "^5.2.0",
    "react-scripts": "^3.4.3",
    "react-timeline-range-slider": "^1.1.2",
    "recharts": "^1.8.5",
    "universal-cookie": "^4.0.3"
  },
  "scripts": {
    "start": "react-scripts start",
    "build": "react-scripts build",
    "build-localhost": "PUBLIC_URL=/ react-scripts build",
    "test": "react-scripts test",
    "eject": "react-scripts eject"
  },
  "eslintConfig": {
    "extends": "react-app"
  },
  "browserslist": {
    "production": [
      ">0.2%",
      "not dead",
      "not op_mini all"
    ],
    "development": [
      "last 1 chrome version",
      "last 1 firefox version",
      "last 1 safari version"
    ]
  },
  "homepage": "https://example.com",
  "proxy": "https://example.com:9000",
  "devDependencies": {
    "dotenv-webpack": "^7.0.2",
    "morris.js": "^0.5.0",
    "raphael": "^2.3.0"
  }
}

Confirming that node.js is in fact listening on port 9000:
bin/www in /server:

/**
 * Get port from environment and store in Express.
 */

var port = normalizePort(process.env.PORT || '9000');
app.set('port', port);

/**
 * Create HTTP server.
 */

var server = http.createServer(app);

/**
 * Listen on provided port, on all network interfaces.
 */

server.listen(port);
server.on('error', onError);
server.on('listening', onListening);

I should note the entire web app is working locally with no issue. I've gone through a number of video tutorials and Stack Overflow answers several times and I've confirmed ufw is configured to allow the necessary ports/traffic and at this point I am out of ideas. Any suggestions on what I'm doing wrong highly appreciated.

brad
  • 197
  • 1
  • 16
  • Had you considered going serverless? The entire node application can be a single Lambda function. – Mordechai Apr 29 '21 at 03:13
  • Is this a development environment? Because in production, you don't need to run anything except of the NodeJS server to have your app running. What do you have on the `3000` port running? If you have Node server running on port `9000` then you just proxy pass Nginx there as you're doing and then you just use the domain without a port `https://example.com/signIn`. – Christos Lytras Apr 29 '21 at 08:02
  • @ChristosLytras this is a production environment; I have built the React app being served on port 3000. If I understand correctly, are you suggesting I try the approach [here](https://create-react-app.dev/docs/deployment/#other-solutions)? – brad May 01 '21 at 12:43
  • @brad you don't need a *static* server when you have a web server like Nginx. What do you mean exactly when you say *"the React app being served on port 3000"*? Being served by what and why? What do you run and bind to the port `3000`? When building for production, all the files are compiled down to Javascript and you don't need anything to start any runtime environment for the client to run like `yarn run start` we're doing for development. – Christos Lytras May 01 '21 at 13:30
  • @ChristosLytras okay my mistake-- nothing is actually running on port 3000. I updated the calls to the backend to use `https://example.com/signIn` instead of `https://example.com:9000/signIn`. I then got the following error: 405 error: not allowed. I looked at [this](https://stackoverflow.com/questions/24415376/post-request-not-allowed-405-not-allowed-nginx-even-with-headers-included#answers-header) answer then updated my nginx config to rewrite the 405 to be 200. The response from the server is now 'You need to enable JavaScript to run this app.' instead of the JSON response I expect to get. – brad May 01 '21 at 16:00
  • Are you sure the proxy is working? Did you try to fetch a static file (*not a javascript, for example a static file `https://example.com/readme.txt`*) using the browser with a successful response? You should not rewrite errors; an error shows that something isn't working. You should only rewrite errors just to drive them to the display error views. Check the nginx log file to see more details regarding that error. Also you should run the backend server without detaching and be able to see the output in the console to catch any unexpected errors. – Christos Lytras May 01 '21 at 17:03
  • @ChristosLytras I am calling the sign in route `https://example.com/signIn` which is a node.js route that will validate the user's credentials and return a response to the React app – brad May 01 '21 at 20:37

3 Answers3

1

1 - Check if your backend is really serving with https

Since you are serving "by your self", on your ubuntu server, you must take care os SSL by your self as well. I see your certbot declaration, but, it's working?

Testing you production backend URL on some tool like sslshopper will confirm that everything is ok.

2 - Ensure that your client is calling through HTTPS as well

Chrome and other browsers will refuse to, from a HTTPS frontend, call a HTTP backend.

Take a look on the network tab, on DevTools, to see if the request happened or not, and if it returned something as well.

3 - Catch your errors (plus)

The "Uncaught" on your console means you didn't treated the API call. Try to do some like:

try{
   // call your API
} catch (error){
  console.error(error);
  // update your local state telling your user that something went wrong
}

Maybe on that console.error log you will could get more information about the real error motive.

Tiago Gouvêa
  • 15,036
  • 4
  • 75
  • 81
0

I don't know what's the case in the production server, but I once faced the same connection error between two cloud services and I fixed it by whitelisting their ip addresses in each other connection security.

Khyar Ali
  • 327
  • 2
  • 8
0
POST https://example.com:9000/signIn net::ERR_SSL_PROTOCOL_ERROR coming 
from one of my React components as well as this error: 
Uncaught (in promise) TypeError: Failed to fetch.

You are making a HTTPS request to a HTTP backend - on port 9000 you have your backend application on HTTP. Your NGINX configuration is listening on HTTPS on /server and does a proxy_pass https://localhost:9000; hence the request from the UI should look like https://example.com/server/signIn.

Also, port 9000 should not be accessible form UI, ideally it should be blocked from firewall and only allow traffic on port 443 (NGINX HTTPS).