[Solved] Meteor 1.8+react-loadable incorrectly fetches from localhost in production

Hi,

I am using using meteor 1.8 on digitalocean with nginx, with route-splitting.
The app works fine on my development machine (localhost).

In the production environment the clients fail with this console error

1c0885fc8105202f1a4d9f3aa488051f85fe3435.js?meteor_js_resource=true:25 Refused to connect to 'http://localhost/__meteor__/dynamic-import/fetch' because it violates the following Content Security Policy directive: "default-src wss: https: data: 'unsafe-inline' 'unsafe-eval' *.google-analytics.com *.googleapis.com *.gstatic.com *.ggpht.com". Note that 'connect-src' was not explicitly set, so 'default-src' is used as a fallback.

My systemctl env has the following environment variables:

NODE_ENV=production
MONGO_URL=mongodb://xxxxx:yyyyy@localhost:27017/zzzzzz?authSource=admin
ROOT_URL=http://localhost
PORT=8080

My nginx server block is

server {                                                              # the app virtual server
  listen [::]:443 ssl http2;
  listen 443 ssl http2;
  server_name app.METEOR_FQDN;                                        # was server_name _;
  root /var/www/html;                                                 # fallback root, if location has no root

  access_log  /var/log/nginx/app.METEOR_NAME.log;
  error_log  /var/log/nginx/app.METEOR_NAME.error.log error;

  include snippets/ssl-METEOR_FQDN.conf;
  include snippets/ssl-params.conf;

  location ~ /.well-known {                                           # case sensitive regular expression mathc; let's encrypt validation
    allow all;
  }

  location = /favicon.ico {                                           # exact match
    root /var/www/app.METEOR_FQDN/html/bundle/programs/web.browser/app;
    access_log off;
    expires 1w;
  }

  location ~* "^/[a-z0-9]{40}\.(css|js)$" {                           # case-insensitive regular expression match
    root /var/www/app.METEOR_FQDN/html/bundle/programs/web.browser;
    access_log off;
    expires max;
  }

  location ~ "^/packages" {                                           # case sensitive regular expression match
    root /var/www/app.METEOR_FQDN/html/bundle/programs/web.browser;
    access_log off;
  }

  location / {                                                        # prefix match
                                                                      # redirect all HTTP traffic to localhost:8080
    proxy_pass http://localhost:8080;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header Host $host;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;

                                                                      # WebSocket support (nginx 1.4)
    proxy_http_version 1.1;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection "upgrade";

                                                                      # allow for stripe webhooks or model uploads (5Mb too big?)
    client_max_body_size 5M;
  }

}

It appears that the client is trying to dynamically fetch portions of the app from the incorrect domain ‘localhost’. The nginx’s content-security-policy however correctly blocks the ‘localhost’ domain.

So somehow the client needs to know that it should not fetch from localhost, but from https://app.METEOR_FQDN

Any advice?

Also do we need to do ‘meteor add dynamic-import’ for meteor v1.8 or was this only required for v1.5?

I’d bet some amount of money that your root cause for this issue is in this env var.

It could very well be.

The thing is, most nginx location server block examples that I have seen take the public domain name and proxy the request to localhost.

I am not sure what the best practice is in this case between, the meteor environment variables, nginx server block location and react-loadable dynamic imports.

An example of a typical nginx server block, that uses a proxy pass to localhost.

What changes should one make if localhost is no longer used with meteor & react-loadable?

You misunderstand. You need to change your ROOT_URL to the actual URL your USER is seeing!

For precisely the reason you’re running into…

And, by the way, you have way too many unnecessary location definitions in there. The only one you actually need is the one for /.

I changed my systemctl .env to:

NODE_ENV=production
MONGO_URL=mongodb://xxxxx:yyyyy@localhost:27017/zzzzzz?authSource=admin
ROOT_URL=https://app.example.net
PORT=8080

I changed the nginx config to:

server_tokens off; # hide nginx version

map $http_upgrade $connection_upgrade { # proxy web-socket connections
        default upgrade;
        ''      close;
}

index index.html index.htm index.nginx-debian.html;

server { # catchall virtual server for http requests; redirect http requests to https
  listen [::]:80;
  listen 80;
  server_name example.net www.example.net app.example.net;
  return 301 https://$server_name$request_uri; # redirect http request (80) to https (443)
}

server { # the app virtual server
  listen [::]:443 ssl http2;
  listen 443 ssl http2;
  server_name app.example.net;
  root /var/www/html; # fallback root, if location has no root

  access_log  /var/log/nginx/app.example.log;
  error_log  /var/log/nginx/app.example.error.log error;

  include snippets/ssl-example.net.conf;
  include snippets/ssl-params.conf;
  location / { # prefix match
    # redirect all HTTP traffic to localhost:8080
    proxy_pass http://localhost:8080;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header Host $host;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;

    # WebSocket support (nginx 1.4)
    proxy_http_version 1.1;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection "upgrade";

    client_max_body_size 5M;
  }

}

The client now gets a ‘bad gateway’ 502.
The app log states:

Error: a route URL prefix must begin with a slash
    at RoutePolicy.declare (packages/routepolicy/routepolicy.js:101:13)
    at new StreamServer (packages/ddp-server/stream_server.js:38:15)
    at new Server (packages/ddp-server/livedata_server.js:1355:24)
    at server_convenience.js (packages/ddp-server/server_convenience.js:6:17)
    at fileEvaluate (packages/modules-runtime.js:336:7)
    at Module.require (packages/modules-runtime.js:238:14)
    at require (packages/modules-runtime.js:258:21)
    at /var/www/app.example.net/html/bundle/programs/server/packages/ddp-server.js:2227:1
    at /var/www/app.example.net/html/bundle/programs/server/packages/ddp-server.js:2234:3
    at /var/www/app.example.net/html/bundle/programs/server/boot.js:411:36
    at Array.forEach (<anonymous>)
    at /var/www/app.example.net/html/bundle/programs/server/boot.js:220:19
    at /var/www/app.example.net/html/bundle/programs/server/boot.js:471:5
    at Function.run (/var/www/app.example.net/html/bundle/programs/server/profile.js:510:12)
    at /var/www/app.example.net/html/bundle/programs/server/boot.js:470:11

I use react-router v4 and my routes are typically:

  {
    key: 'index',
    path: '/',
    exact: true,
    component: Loadable({
      loader: () => import('../pages/signin'),
      loading
    })
  },
  {
    key: 'signIn',
    path: '/signin',
    exact: true,
    component: Loadable({
      loader: () => import('../pages/signin'),
      loading
    })
  },

and the main app component’s render() has this look:

    <BrowserRouter>
      <Switch>
        <Route component={NotFound} />
      </Switch>
    </BrowserRouter>

with NotFound defined as:

import React, {PureComponent} from 'react';

import {Alert} from 'reactstrap';
import ResponsiveContainer from '../components/responsive-container';
import Logo from '../components/logo';

class NotFound extends PureComponent {
  render() {
    const form = [
      <Logo key="logo" />,
      <Alert color="danger">{`Error [404]: ${window.location.pathname} does not exist`}</Alert>
    ];

    return <ResponsiveContainer>{form}</ResponsiveContainer>;
  }
}

export default NotFound;

The problem was that my systemd.env file did not work with an inline comment for the ROOT_URL.

This failed:
ROOT_URL=https://app.example.net #http://localhost

This works:
ROOT_URL=https://app.example.net

Interestingly my MAIL_URL has an inline comment, but I have not experienced issues with it.

Thanks

What if you want to access the app through multiple hostnames? With this configuration all fetch calls ask for https://app.example.net even if the app is accessed through another domain.

EDIT: Would setting ROOT_URL="/" work?