404 on route "/forest/authentication" during init with serverless hosting

Feature(s) impacted

Agent deployment on serverless hosting

Observed behavior

I want to deploy the Forest Admin agent on Google Cloud Run. It’s a serverless service, so when the instance is not used for a moment, it’s shut down. And it’s re-instantiate on demand.

That’s the theory. When the agent starts, its returns a 404 on the route “/forest/authentication” for at least 3 seconds. Until it is initiated.

Because of that, I get a error on the ForestAdmin app on the first page load. The error disappear if I refresh the page (because the agent is loaded).

Expected behavior

Is there a way to just way for the app to be ready to resolve the promise on “/forest/authentication”?
I don’t care if the loading takes 4 seconds the first time. I just don’t want to have an error.

Context

  • Project name: mfc-staging
  • Team name: Operation
  • Environment name: Staging Online
  • Agent type & version: forest-express-sequelize v9

I’ve managed to do what I wanted following the post in Error when logging in on Forest with admin backend deployed on AWS in lambda function - #16 by Cyril_Limam

I’ve updated to app.js file to return an async createApp function. And the server.js to use “const app = await createApp();”

It works now :ok_hand:

1 Like

Thanks a lot for sharing the solution!

If needed, here is my new app.js file

const express = require('express');
const requireAll = require('require-all');
const path = require('path');
const cookieParser = require('cookie-parser');
const bodyParser = require('body-parser');
const cors = require('cors');
const jwt = require('express-jwt');
const morgan = require('morgan');
const {
  errorHandler,
  ensureAuthenticated,
  PUBLIC_ROUTES,
} = require('forest-express-sequelize');

async function createApp(){
  const app = express();

  let allowedOrigins = [/\.forestadmin\.com$/, /localhost:\d{4}$/];

  if (process.env.CORS_ORIGINS) {
    allowedOrigins = allowedOrigins.concat(process.env.CORS_ORIGINS.split(','));
  }

  const corsConfig = {
    origin: allowedOrigins,
    maxAge: 86400, // NOTICE: 1 day
    credentials: true,
  };

  app.use(morgan('tiny'));
  // Support for request-private-network as the `cors` package
  // doesn't support it by default
  // See: https://github.com/expressjs/cors/issues/236
  app.use((req, res, next) => {
    if (req.headers['access-control-request-private-network']) {
      res.setHeader('access-control-allow-private-network', 'true');
    }
    next(null);
  });
  app.use('/forest/authentication', cors({
    ...corsConfig,
    // The null origin is sent by browsers for redirected AJAX calls
    // we need to support this in authentication routes because OIDC
    // redirects to the callback route
    origin: corsConfig.origin.concat('null')
  }));
  app.use(cors(corsConfig));
  app.use(bodyParser.json());
  app.use(bodyParser.urlencoded({ extended: false }));
  app.use(cookieParser());
  app.use(express.static(path.join(__dirname, 'public')));

  app.use(jwt({
    secret: process.env.FOREST_AUTH_SECRET,
    credentialsRequired: false,
    algorithms: ['HS256'],
  }));

  app.use('/forest', (request, response, next) => {
    if (PUBLIC_ROUTES.includes(request.url)) {
      return next();
    }
    return ensureAuthenticated(request, response, next);
  });

  requireAll({
    dirname: path.join(__dirname, 'routes'),
    recursive: true,
    resolve: (Module) => app.use('/forest', Module),
  });

  const middlewares = requireAll({
    dirname: path.join(__dirname, 'middlewares'),
    recursive: true,
    resolve: (Module) => Module(app),
  });

  app.use(errorHandler());

  await Promise.all(Object.keys(middlewares).map((key) => middlewares[key]));

  return app;
}
  

module.exports = createApp;

And server.js file:

require('dotenv').config();
const debug = require('debug')('{name}:server');
const http = require('http');
const chalk = require('chalk');
const createApp = require("./app");

function normalizePort(val) {
  const port = parseInt(val, 10);

  if (Number.isNaN(port)) { return val; }
  if (port >= 0) { return port; }

  return false;
}

async function createServer() {
  const port = normalizePort(process.env.PORT || process.env.APPLICATION_PORT || '3310');

  const app = await createApp();
  app.set('port', port);

  const server = http.createServer(app);
  server.listen(port);

  function onError(error) {
    if (error.syscall !== 'listen') {
      throw error;
    }

    const bind = typeof port === 'string'
      ? `Pipe ${port}`
      : `Port ${port}`;

    switch (error.code) {
      case 'EACCES':
        console.error(`${bind} requires elevated privileges`);
        process.exit(1);
        break;
      case 'EADDRINUSE':
        console.error(`${bind} is already in use`);
        process.exit(1);
        break;
      default:
        throw error;
    }
  }

  function onListening() {
    const addr = server.address();
    const bind = typeof addr === 'string'
      ? `pipe ${addr}`
      : `port ${addr.port}`;
    debug(`Listening on ${bind}`);
  
    console.log(chalk.cyan(`Your application is listening on ${bind}.`));
  }
  
  server.on('error', onError);
  server.on('listening', onListening);
}

createServer();
1 Like