Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

React Flask Heroku App is not displaying frontend

Summary of the issue

I've recently tried to deploy my local application to Heroku. It's built with a Flask backend and a React/Redux frontend. After working through the intricacies of Heroku (procfiles, where it reads package.json, etc.) I'm able to get the backend to show (example: the flask-admin section is working as well as my database), but I'm still unable to reach the frontend (react) portion of my site. There are no errors that I can spot in the Heroku logs and on local version my application works perfectly fine when I start up my python server and do NPM start in the static directory.

Any idea why the front end wouldn't be showing or how to access it?


Logs:

I've removed some sensitive information from the details, but here's what heroku logs --tail gives me when I try to refresh the app.

2018-02-11T01:18:01.000000+00:00 app[api]: Build succeeded
2018-02-11T01:21:12.305017+00:00 heroku[web.1]: Starting process with command `gunicorn main:app`
2018-02-11T01:21:16.374150+00:00 heroku[web.1]: State changed from starting to up
2018-02-11T01:21:15.948707+00:00 app[web.1]: [2018-02-11 01:21:15 +0000] [4] [INFO] Starting gunicorn 19.6.0
2018-02-11T01:21:15.949430+00:00 app[web.1]: [2018-02-11 01:21:15 +0000] [4] [INFO] Listening at: http://0.0.0.0:29162



2018-02-11T01:21:19.278810+00:00 heroku[router]: at=info method=GET path="/" host=removed.herokuapp.com request_id=bd74ea4c-3e3a-403b-8850-198b7dec20e2 fwd="104.152.1.62" dyno=web.1 connect=1ms service=2146ms status=200 bytes=2895 protocol=https
2018-02-11T01:21:19.650759+00:00 heroku[router]: at=info method=GET path="/dist/bundle.css" host=removed.herokuapp.com request_id=48183249-fb12-4c7a-9a53-2a57ab58d89b fwd="104.152.1.62" dyno=web.1 connect=0ms service=3ms status=200 bytes=2895 protocol=https
2018-02-11T01:21:19.816113+00:00 heroku[router]: at=info method=GET path="/dist/bundle.js" host=removed.herokuapp.com request_id=1c8b258b-4187-4df6-af35-784e62fb97e5 fwd="104.152.1.62" dyno=web.1 connect=1ms service=3ms status=200 bytes=2895 protocol=https

When I view source I see the code from my index.html file (in the static folder, which is correct) and this particular line is highlighted red which makes me think it's missing bundle.js? <script src="/dist/bundle.js"></script>

When I try to visit the front end, none of it is showing. I'm really unsure where to look next.

Initial ideas:

  1. Is my server.js file actually starting on Heroku? I have a "start" script in my package.json, but if procfile is executing Python...does it actually start? How do I make it start without putting it into a postinstall or post-build step?

  2. Is webpack messing something up. My production webpack is slightly different from my development so maybe during build:production it's getting messed up? However, that doesn't explain why build is always succeeding.

  3. Is my server.js or webpack forcing formatting on the index.html file and so it can't parse properly? Perhaps this is why I'm seeing SyntaxError: expected expression, got '< bundle.js:1 in the console?

Update: I received a note from the Heroku support team letting me know that Heroku dynos are not built to support multi processes applications like mine. As a result, they feel like I'd need to make some signficant changes to have all the HTTP requests forwarding through one dyno. Perhaps this is my issue?


Here are files that may help debug:

Server.js

const http = require('http');
const express = require('express');
const httpProxy = require('http-proxy');
const path = require('path');
const apiPort = process.env.PORT || 8081;

const proxy = httpProxy.createProxyServer({});

const app = express();

app.use(require('morgan')('short'));

(function initWebpack() {
    const webpack = require('webpack');
    const webpackConfig = require('./webpack/common.config');

    const compiler = webpack(webpackConfig);

    app.use(require('webpack-dev-middleware')(compiler, {
        noInfo: true, publicPath: webpackConfig.output.publicPath,
    }));

    app.use(require('webpack-hot-middleware')(compiler, {
        log: console.log, path: '/__webpack_hmr', heartbeat: 10 * 1000,
    }));

    app.use(express.static(path.join(__dirname, '/')));
}());


 app.all(/^\/api\/(.*)/, (req, res) => {
    proxy.web(req, res, { target: `http://0.0.0.0:${apiPort}` });
});

app.get(/.*/, (req, res) => {
    res.sendFile(path.join(__dirname, '/index.html'));
});


const server = http.createServer(app);
server.listen(process.env.PORT || 8080, () => {
    const address = server.address();
    console.log('Listening on: %j', address);
    console.log(' -> that probably means: http://0.0.0.0:%d', address.port);
});

File Structure

ROOT
├──/application
│   ├── models.py
│   ├── app.py
├──/static
│   ├──/bin
│   ├──/dist
│   │   ├──bundle.js
│   ├──/node_modules
│   ├──/src
│   │   ├──/actions
│   │   ├──/components
│   │   │   ├──/examplecomponenthere
│   │   │   │   ├──index.js (for example component)
│   │   ├──/constants
│   │   ├──/containers
│   │   ├──/reducers
│   │   ├──/store
│   │   ├──/webpack
│   ├──index.html
│   ├──package.json (the true one)
│   ├──server.js
├──/tests
├──config.py
├──index.py
├──main.py
├──package.json (one to help heroku start)
├──procfile
├──requirements.txt.
├──setup.py
├──tests.py

Package.json in root This file exists because I'm running a multi build. Heroku doesn't seem to recognize the package.json in static until I use this one to push it over there.

    { 
"name": "rmmd", 
"version": "0.0.1", 
"engines": { 
"node": "6.11.1", 
"npm": "3.10.10" 
}, 
"scripts": { 
"start": "node static/bin/server.js", 
"heroku-postbuild": "cd static && npm install && npm run build:production"
 } 
}

Package.json in static

{
  "name": "redux-easy-boilerplate",
  "version": "1.3.3",
  "description": "",
  "scripts": {
    "clean": "rimraf dist",
    "build": "webpack --progress --verbose --colors --display-error-details --config webpack/common.config.js",
    "build:production": "npm run clean && npm run build",
    "lint": "eslint src",
    "start": "node bin/server.js",
    "test": "karma start"
  },
  "repository": {
    "type": "git",
    "url": ""
  },
  "keywords": [
    "react",
    "reactjs",
    "boilerplate",
    "redux",
    "hot",
    "reload",
    "hmr",
    "live",
    "edit",
    "webpack"
  ],
  "author": "https://github.com/anorudes, https://github.com/keske",
  "license": "MIT",
  "devDependencies": {
    "webpack-dev-middleware": "^1.5.0",
    "webpack-dev-server": "^1.14.1",
    "webpack-hot-middleware": "^2.6.0",
  },
  "dependencies": {
    "ant-design-pro": "^0.3.1",
    "antd": "^3.0.0",
    "lodash": "^4.17.4",
    "prop-types": "^15.6.0",
    "react-bootstrap": "^0.31.0",
    "redux-devtools-extension": "^2.13.2",
"autoprefixer": "6.5.3",
    "axios": "^0.15.3",
    "babel-core": "^6.4.5",
    "babel-eslint": "^7.1.1",
    "babel-loader": "^6.2.1",
    "babel-plugin-import": "^1.2.1",
    "babel-plugin-react-transform": "^2.0.0",
    "babel-plugin-transform-decorators-legacy": "^1.3.4",
    "babel-polyfill": "^6.3.14",
    "babel-preset-es2015": "^6.3.13",
    "babel-preset-react": "^6.3.13",
    "babel-preset-react-hmre": "^1.0.1",
    "babel-preset-stage-0": "^6.3.13",
    "bootstrap": "^3.3.5",
    "bootstrap-loader": "^1.2.0-beta.1",
    "bootstrap-sass": "^3.3.6",
    "bootstrap-webpack": "0.0.5",
    "classnames": "^2.2.3",
    "css-loader": "^0.26.4",
    "csswring": "^5.1.0",
    "deep-equal": "^1.0.1",
    "eslint": "^3.4.0",
    "eslint-config-airbnb": "13.0.0",
    "eslint-plugin-import": "^2.2.0",
    "eslint-plugin-jsx-a11y": "^3.0.1",
    "eslint-plugin-react": "^6.1.2",
    "expect": "^1.13.4",
    "exports-loader": "^0.6.2",
    "expose-loader": "^0.7.1",
    "express": "^4.13.4",
    "express-open-in-editor": "^1.1.0",
    "extract-text-webpack-plugin": "^1.0.1",
    "file-loader": "^0.9.0",
    "gapi": "0.0.3",
    "history": "^4.4.1",
    "http-proxy": "^1.12.0",
    "imports-loader": "^0.6.5",
    "jasmine-core": "^2.4.1",
    "jquery": "^3.1.0",
    "jwt-decode": "^2.1.0",
    "karma": "^1.2.0",
    "karma-chrome-launcher": "^2.0.0",
    "karma-mocha": "^1.1.1",
    "karma-webpack": "^1.7.0",
    "less": "^2.7.2",
    "less-loader": "^2.2.3",
    "lodash": "^4.5.1",
    "material-ui": "^0.16.4",
    "mocha": "^3.0.2",
    "morgan": "^1.6.1",
    "node-sass": "^3.4.2",
    "postcss-import": "^9.0.0",
    "postcss-loader": "^1.1.1",
    "q": "^1.4.1",
    "qs": "^6.1.0",
    "rc-datepicker": "^4.0.1",
    "react": "^15.3.1",
    "react-addons-css-transition-group": "^15.3.1",
    "react-bootstrap": "^0.31.0",
    "react-calendar-component": "^1.0.0",
    "react-date-picker": "^5.3.28",
    "react-datepicker": "^0.37.0",
    "react-document-meta": "^2.0.0-rc2",
    "react-dom": "^15.1.0",
    "react-forms": "^2.0.0-beta33",
    "react-hot-loader": "^1.3.0",
    "react-loading-order-with-animation": "^1.0.0",
    "react-onclickoutside": "^5.3.3",
    "react-redux": "^4.3.0",
    "react-router": "3.0.0",
    "react-router-redux": "^4.0.0",
    "react-tap-event-plugin": "^2.0.1",
    "react-transform-hmr": "^1.0.1",
    "redux": "^3.2.1",
    "redux-form": "^6.0.1",
    "redux-logger": "2.7.4",
    "redux-thunk": "^2.1.0",
    "resolve-url-loader": "^1.4.3",
    "rimraf": "^2.5.0",
    "sass-loader": "^4.0.0",
    "style-loader": "^0.13.0",
    "url-loader": "^0.5.7",
    "webpack": "^1.12.11",
    "webpack-merge": "^1.0.2",
    "yargs": "^6.5.0"
  }
}

Webpack Prod

const webpack = require('webpack');
const ExtractTextPlugin = require('extract-text-webpack-plugin');

module.exports = {
  devtool: 'source-map',

  entry: ['bootstrap-loader/extractStyles', './src/index'],
  output: {
    publicPath: '/dist/',
  },

  module: {
    loaders: [
      {
        test: /\.scss$/,
        loader: 'style!css!postcss-loader!sass',
      },
    ],
  },

  plugins: [
    new webpack.DefinePlugin({
      'process.env': {
        NODE_ENV: '"production"',
      },
      __DEVELOPMENT__: false,
    }),
    new ExtractTextPlugin('bundle.css'),
    new webpack.optimize.DedupePlugin(),
    new webpack.optimize.OccurenceOrderPlugin(),
    new webpack.optimize.UglifyJsPlugin({
      compress: {
        warnings: false,
      },
    }),
  ],
};

Webpack common

const path = require('path');
const autoprefixer = require('autoprefixer');
const postcssImport = require('postcss-import');
const merge = require('webpack-merge');

const development = require('./dev.config');
const production = require('./prod.config');

require('babel-polyfill').default;

const TARGET = process.env.npm_lifecycle_event;

const PATHS = {
    app: path.join(__dirname, '../src'),
    build: path.join(__dirname, '../dist'),
};

process.env.BABEL_ENV = TARGET;

const common = {
    entry: [
        PATHS.app,
    ],

    output: {
        path: PATHS.build,
        filename: 'bundle.js',
    },

    resolve: {
        extensions: ['', '.jsx', '.js', '.json', '.scss'],
        modulesDirectories: ['node_modules', PATHS.app],
    },

    module: {
        loaders: [{
            test: /bootstrap-sass\/assets\/javascripts\//,
            loader: 'imports?jQuery=jquery',
        }, {
            test: /\.woff(\?v=\d+\.\d+\.\d+)?$/,
            loader: 'url?limit=10000&mimetype=application/font-woff',
        }, {
            test: /\.woff2(\?v=\d+\.\d+\.\d+)?$/,
            loader: 'url?limit=10000&mimetype=application/font-woff2',
        }, {
            test: /\.ttf(\?v=\d+\.\d+\.\d+)?$/,
            loader: 'url?limit=10000&mimetype=application/octet-stream',
        }, {
            test: /\.otf(\?v=\d+\.\d+\.\d+)?$/,
            loader: 'url?limit=10000&mimetype=application/font-otf',
        }, {
            test: /\.eot(\?v=\d+\.\d+\.\d+)?$/,
            loader: 'file',
        }, {
            test: /\.svg(\?v=\d+\.\d+\.\d+)?$/,
            loader: 'url?limit=10000&mimetype=image/svg+xml',
        }, {
            test: /\.js$/,
            loaders: ['babel-loader'],
            exclude: /node_modules/,
        }, {
            test: /\.css$/,
            include: /node_modules/,
            loaders: ['style-loader', 'css-loader'],
        }, {
            test: /\.png$/,
            loader: 'file?name=[name].[ext]',
        }, {
            test: /\.jpg$/,
            loader: 'file?name=[name].[ext]',
        }],
    },

    postcss: (webpack) => (
        [
            autoprefixer({
                browsers: ['last 2 versions'],
            })
        ]
    ),
};

if (TARGET === 'start' || !TARGET) {
    module.exports = merge(development, common);
}

if (TARGET === 'build' || !TARGET) {
    module.exports = merge(production, common);
}

ProcFile

web: gunicorn main:app

Buildpacks in Heroku

heroku buildpacks:set heroku/python
heroku buildpacks:add heroku/nodejs
like image 884
dizzy Avatar asked Feb 09 '18 20:02

dizzy


2 Answers

So! It turns out Heroku support team was incorrect in their analysis of my application. My application is built in two different ways (one for production as well as for development). Using npm run start [see static/package.json] on local utilizes hot reloading and benefits from faster local changes via server.js. However, in a production environment, you want to use a compressed bundle.js file so my goal was to use npm run build:production [see static/package.json].

The issue I was running into was SyntaxError: expected expression, got '< bundle.js:1 in the console and it seemed to me that bundle.js wasn't loading at all. I listed a series of valid questions above on why I thought that might happen, but they all assumed that the main problem was an inability to run my react application at the same time as my flask application.

I was totally wrong. I didn't need to run server.js at all. The REAL reason that index.html and flask/python wasn't able to find my bundle.js and load the frontend on production was because of a mistake in the config.py file within flask which I never thought to post.

Flask has a very particular configuration that allows static_folder to be defined and template_folder. A while back I had swapped my static_folder for another directory while working on some image upload functionality. The reason I never caught it is because on local I run server.js for hot reloading so I never saw the compressed bundle.js file error out.

After fixing this mistake, I pushed to heroku and amazingly...it worked on the first try!

Here's the correct code that fixed it:

app = Flask(__name__, static_folder="./static/dist", template_folder="./static")

In closing whilst running a flask/react application on Heroku:

  • Use multi buildpacks (one for node, one for python).
  • Use your procfile to load only the flask side.
  • You need a package.json file in your root directory...even if you have another in your static folder like me.
  • Use webpack to compress your react code and serve it up in a compressed way.
  • Use render_template with flask to render the index.html file that holds you root div for react.
  • make sure all your dependencies for React are listed inside of actual "dependencies" instead of "devdepencies" otherwise Heroku will ignore them.

I really hope this helps someone! I was slamming my head against the wall for 2 weeks and it turned out to be a small obscure problem....isn't it always?

Additional resources: https://codeburst.io/creating-a-full-stack-web-application-with-python-npm-webpack-and-react-8925800503d9 While this is very simplified...it's what lead me to finding my bug so I'll post it here.

like image 90
dizzy Avatar answered Sep 21 '22 16:09

dizzy


Your flask backend doesn't seem to be running on the same port as you forward your traffic too:

Starting gunicorn 19.6.0 Listening at: 0.0.0.0:13521 (4) 

app.all(/^\/api\/(.*)/, (req, res) => {
    proxy.web(req, res, { target: 'http://0.0.0.0:8081' });
});

Flask is listening on port 13521 (chosen randomly) but the /api/ routes are forwarded to port 8081. Instead, you should probably configure gunicorn to use a fixed port: http://docs.gunicorn.org/en/latest/settings.html#bind

like image 25
sahuk Avatar answered Sep 18 '22 16:09

sahuk