Next.js: publicRuntimeConfig undefined in staging environment

Created on 18 Mar 2018  路  27Comments  路  Source: vercel/next.js

  • [x] I have searched the issues of this repository and believe that this is not a duplicate.

Expected Behavior

Steps to Reproduce (for bugs)

next.config.js

module.exports = (phase, { defaultConfig }) => {
  return {
    publicRuntimeConfig: {
      defaultEnvironmentSettings: 'test',
    },
    webpack: (config, { buildId, dev, isServer, defaultLoaders }) => {
      // XXX https://github.com/evanw/node-source-map-support/issues/155
      config.node = {
        fs: 'empty',
        module: 'empty',
      };
      return config;
    },
  };
};

page

const { publicRuntimeConfig } = getConfig();
const { groupSettings, url } = this.props;
const schoolName = url.query.schoolName;
console.log('publicRuntimeConfig', publicRuntimeConfig) // publicRuntimeConfig undefined
console.log('getConfig()', getConfig()) // getConfig()  serverRuntimeConfig:{}, publicRuntimeConfig: undefined

Context

publicRuntimeConfig is undefined

This only happens when I use NODE_ENV staging, it works fine in development mode. (I haven't tested against production env)

Note that my staging environment is hosted on AWS lambda, but it's maybe not related to the issue.

Most helpful comment

@wescoder I had the same issue on the test env, but it makes sense because next server doesn't run during tests. In that case, I simply changed the getConfig() call to:

const { publicRuntimeConfig = {} } = getConfig() || {};

That nulls out the values, you can set appropriate defaults if you need them during tests, we don't in our case.

I still cannot repro the issue in this thread. We now have this config running in production - out to ~2 million users, built, exported, deployed to CDN configured using assetPrefix using static JS bundles and a custom node server - without issue.

All 27 comments

Is there any more information I can provide to help solve this issue? It's quite a critical one.

@Vadorequest I've just tested this case myself and had no issue. I also spent an hour walking through the entire code path in the Next source to see what it might be and the thing is, the loading of those variables is never dependent on the NODE_ENV (or dev, or any other env-specific value). Since we have the same use case, I need to make sure this works.

Not sure what to tell you. What version of the canary are you on? The relevant change is in 5.0.1-canary 10. I'm testing with 17.

@codinronan I'm using 5.0.1-canary.16.

I also checked the source code and didn't see anything that may be the source of this bug.
When debugging in my code, what I can tell is that it works in my local environment, publicRuntimeConfig contains the expected data. But as soon as I deploy my code on AWS, it becomes undefined.

Indeed, the NODE_ENV may not be related at all, and in this case it's likely a behaviour related to AWS hosting?

Thank you for your feedback!

Just deployed a create-next-app with the code from your issue: https://create-next-example-app-nagabdohry.now.sh/ as you can see in the console it's working fine.

Okay, thanks for your feedbacks. So, the issue is either related to:

  • Next js build (doesn't seem to be the case since it works when hosted on AWS through now)
  • Serverless packaging (which takes Next's build and zip it)
  • Serverless deployment (which takes the zip and send it to aws)
  • AWS hosting environment

I don't see any other thing that may impact this feature. I'll try to investigate each of those.

The source of the problem is that __NEXT__.runtimeConfig is undefined on AWS. It's behaving as if I didn't had anything in next.config.js

I don't specify a config when starting my next.js app:

const nextAppConfig = {
  dev: !isHostedOnAWS() && process.env.NODE_ENV === 'development',
  quiet: false //!(process.env.NODE_ENV === 'development' || process.env.NODE_ENV === 'staging')
};
const nextApp = next(nextAppConfig);

Maybe Next is failing at resolving the config on AWS?

I changed the file node_modules/next/dist/lib/runtime-config.js

function setConfig(configValue) {
  console.log('next:runtime-config:setConfig()', configValue) // added this line
  runtimeConfig = configValue;
}

When I run npm run next or NODE_ENV=staging npm run next, I get the same following output:

next:runtime-config:setConfig() { serverRuntimeConfig: { serverRuntimeConfigValue: 'test server' },
  publicRuntimeConfig: 
   { GROUP_NAME: undefined,
     publicRuntimeConfigValue: 'test public' } }

So, I can tell it's not related to the NODE_ENV.


When I run next build or NODE_ENV=staging next build, I get the same output in both cases, but there is no log about next:runtime-config:setConfig(). Therefore, I assume next:runtime-config:setConfig() is only called at runtime? (would make sense, based on the name of the file)


When I run npm run deploy:hep

With the following scripts in package.json:

  "scripts": {
    "build:hep": "cross-env-shell GROUP_NAME=staging.hep NODE_ENV=staging \"next build && serverless package -s hepStaging\"",
    "deploy:hep": "cross-env-shell GROUP_NAME=staging.hep NODE_ENV=staging \"npm run build:hep && serverless deploy -v --package .serverless -s hepStaging\"",
  },

I don't get any log about next:runtime-config:setConfig() in this case either.

When I go on my website at https://staging.hep.loan-advisor.studylink.fr/ I don't see any log about next:runtime-config:setConfig() either on the browser console, nor on the server console. (isn't that weird? There should be, if setConfig is indeed called at runtime, I should get logs on the server console)

@codinronan @timneutkens I found a proper workaround.

The issue is that Next is failing at resolving the next.config.js, I don't know why, but that's the reason.

If I provide a conf object when starting next, it works like a charm.

Here is what I changed to make it work:

I created a next.runtimeConfig.js with the following:

const serverRuntimeConfig = {
  serverRuntimeConfigValue: 'test server'
};

const publicRuntimeConfig = {
  GROUP_NAME: process.env.GROUP_NAME,
  publicRuntimeConfigValue: 'test public'
};

module.exports = {
  serverRuntimeConfig,
  publicRuntimeConfig,
};


I changed my next.config.js to the following:

// const { PHASE_DEVELOPMENT_SERVER } = require('next/constants');
const { serverRuntimeConfig, publicRuntimeConfig } = require('./next.runtimeConfig');

module.exports = (phase, { defaultConfig }) => {
  return {
    serverRuntimeConfig,
    publicRuntimeConfig,
    webpack: (config, { buildId, dev, isServer, defaultLoaders }) => {
      // XXX https://github.com/evanw/node-source-map-support/issues/155
      config.node = {
        fs: 'empty',
        module: 'empty',
      };
      return config;
    },
  };
};


I changed my app.js to force include a conf when starting the server:

...
import { serverRuntimeConfig, publicRuntimeConfig } from '../../../next.runtimeConfig';

const nextConf = {
  serverRuntimeConfig,
  publicRuntimeConfig,
};

// XXX next.dev enables HMR, which we don't want if not in development mode, or when we are on AWS's infrastructure
const nextAppConfig = {
  dev: !isHostedOnAWS() && process.env.NODE_ENV === 'development',
  conf: nextConf,
  quiet: false,
};
const nextApp = next(nextAppConfig);

With those changes, it works properly on AWS and I get a __NEXT_DATA__ with runtimeConfig populated.

I don't know what is the cause of the bug.. And I'm just thinking that maybe providing a conf isn't optional when starting Next through Express. I had assumed it was optional but maybe it's not? (but in this case... why does it work with my local express and not on AWS?)

If that the case, I guess the doc should be updated to make it very clear. Since Next.js auto-resolves the next.config.js on its own, I was expecting it to do it as well when started from Express or another server.

@Vadorequest what does your build/deploy stack look like? When it is running on AWS, are you using next start or a custom server? Are you using build + export or are you running as if it were on dev?

Are you using assetPrefix?

@codinronan Since you're asking, I guess the encountered behaviour isn't the expected behaviour?

I am running Next.js on AWS Lambda, in non-dev mode, I basically build the Next app, which is then loaded by an Express server and served by AWS. You can find the repository there: https://github.com/Vadorequest/serverless-with-next

The scripts that are of interest are there: https://github.com/Vadorequest/serverless-with-next/blob/master/package.json#L14 (Lines 9 and 14)

And the Next/Express startup is there https://github.com/Vadorequest/serverless-with-next/blob/master/src/functions/server/app.js#L12

It's not the actual repository I'm working on, but it's the closer I've got because that's kinda my playground for Next+Express+AWS apps.

I'm having the same issues, but instead on test environment when running jest tests.
If you want to see another repo for reference, my repo is here

@wescoder I had the same issue on the test env, but it makes sense because next server doesn't run during tests. In that case, I simply changed the getConfig() call to:

const { publicRuntimeConfig = {} } = getConfig() || {};

That nulls out the values, you can set appropriate defaults if you need them during tests, we don't in our case.

I still cannot repro the issue in this thread. We now have this config running in production - out to ~2 million users, built, exported, deployed to CDN configured using assetPrefix using static JS bundles and a custom node server - without issue.

@codinronan Yeah it makes sense, I tested it this way and it worked fine. Thanks!

@codinronan I hear you, maybe it's related to the way AWS lambda stores files or something related to their environment, which isn't the traditional server most people use. I really don't know.

Anyway, since I found a proper workaround, by loading the config explicitly, it isn't a real issue anymore.

Thank you for your help.

If you run into issues where publicRuntimeConfig is undefined within testcases, you can use the following in a setup file;

// jest.setup.js
import { setConfig } from 'next/config'
import config from './next.config'

// Make sure you can use "publicRuntimeConfig" within tests.
setConfig(config.publicRuntimeConfig)

In case it helps others, my solution (derived from @Vadorequest) follows.

I determine which "environment" to pull the runtime info from, based on whether the code runs on the server or browser.

import getConfig from 'next/config'

const amBrowser = typeof window !== 'undefined'

let ENV
let PORT
if (amBrowser) {
  const config = getConfig().publicRuntimeConfig
  ENV = config.ENV
  PORT = config.PORT
}
else {
  ENV = process.env.NODE_ENV || 'development'
  PORT = process.env.PORT || 8080
}

I'm running a custom Express server. Here's my snippet for creating the next app --

const nextConf = {
  serverRuntimeConfig: {},
  publicRuntimeConfig: { ENV, PORT },
}

const DEV = ENV === 'development'
const nextApp = next({ dev:DEV, conf:nextConf, quiet:false })

Right after nextApp is created, getConfig() will return what you expected. Before that point, getConfig() returns undefined.

@robinvdvleuten what worked for me was:

// jest.setup.js
import { setConfig } from 'next/config'
import { publicRuntimeConfig } from './next.config'

// Make sure you can use "publicRuntimeConfig" within tests.
setConfig({ publicRuntimeConfig })

@joaovieira yes that's exactly the same as my code 馃憤

For reference, I was required to mock the next/config singleton to get this working in a Jest test environment. It seems to be required due the way Jest manages its own require.cache, but I'm not sure why no one else has run into this in regards to next/config. Anyways, the below works for me.

// jest.setup.js
import mockEnvConfig from '~/env-config.js';

jest.mock('next/config', () => () => ({ publicRuntimeConfig: mockEnvConfig }));

// jest.config.js
module.exports = {
  setupFiles: ['<rootDir>/jest.setup.js'],
};

@dcalhoun Your answer was very helpful as it solved a similar publicRuntimeConfig error I was receiving, so thank you!!

Came here with the same publicRuntimeConfig issue. I'm new to testing and NextJS, so it's been quite a journey figuring out how and what to test (especially since React, Redux, and other dependencies are mixed in). When I replaced my jest.setup.js file with @robinvdvleuten's suggestion, the publicRuntimeConfig issue was resolved:

// jest.setup.js
import { setConfig } from 'next/config'
import { publicRuntimeConfig } from './next.config'

// Make sure you can use "publicRuntimeConfig" within tests.
setConfig({ publicRuntimeConfig })

Then my other tests stopped working. I ended up including both Enzyme and Next configurations in my file and now they both work. Here's how I configured it in my jest.setup.js file in case anyone ever finds themselves in my shoes:

import { setConfig } from 'next/config';
import config from './next.config';

import { configure } from 'enzyme';
import Adapter from 'enzyme-adapter-react-16';

// NextJS 
setConfig(config.publicRuntimeConfig);

// Enzyme 
configure({ adapter: new Adapter() });

Though i wonder if down the line i'll need to use @dcalhoun configuration solution...

Just another one to this thread. After a long time struggling to understand why my next/config manual mock wasn't being run, I found out that after transpilation (babel-jest) the module is renamed to next-server/config (re-exported in https://github.com/zeit/next.js/blob/v8.0.3/packages/next/config.js) and thus not the same module I was mocking. I.e.:

// path.ts
import getConfig from 'next/config';

const { publicRuntimeConfig } = getConfig();
const { assetPrefix = '', basePath = '' } = publicRuntimeConfig;
// path.test.ts
jest.mock('next/config', () => {
  console.log('this is never run!');
  return () => ({
    publicRuntimeConfig: {
      assetPrefix: 'https://example.com',
      basePath: '/base',
    },
 };
}));

import { assetPath, appPath } from './path';

// transpiled path.js
var _interopRequireDefault = require('@babel/runtime-corejs2/helpers/interopRequireDefault');

Object.defineProperty(exports, '__esModule', {
  value: true,
});
exports.appPath = exports.assetPath = void 0;

// ----------------------- HERE IT IS!! ----------------------------
var _config = _interopRequireDefault(require('next-server/config'));

var _getConfig = (0, _config.default)(),
  publicRuntimeConfig = _getConfig.publicRuntimeConfig;

var _publicRuntimeConfig$ = publicRuntimeConfig.assetPrefix,
  assetPrefix = _publicRuntimeConfig$ === void 0 ? '' : _publicRuntimeConfig$,
  _publicRuntimeConfig$2 = publicRuntimeConfig.basePath,
  basePath = _publicRuntimeConfig$2 === void 0 ? '' : _publicRuntimeConfig$2;
// transpiled path.test.js
jest.mock('next/config', function() {
  console.log('this is never run!');
  return function() {
    return {
      publicRuntimeConfig: {
        assetPrefix: 'https://example.com',
        basePath: '/base',
      },
    };
  };
});

var _path = require('./path');

Changing the mock to jest.mock('next-server/config') works as expected... This is horrible to diagnose and I believe this worked correctly before, so not sure something changed in either babel or babel-jest to cause the optimization.

@joaovieira this is solved on next@canary

Thanks @timneutkens! Always one step ahead. Would it be too hard to find the commit/PR that fixed it? Interested to know how it was solved.

@joaovieira https://github.com/zeit/next.js/pull/6458, basically what this does is bundle the wrapper module and mark the require for next-server/config etc as external in webpack. That way it achieves the same without doing code modification 馃憤

I know this closed but to the original issue of having publicRuntimeConfig undefined in non-dev environments, disabling automatic static optimisation worked for me in this case i.e. overriding _app.js and uncommenting the getInitialProps function. Might be a temporary fix though.

import React from 'react';
import App from 'next/app';

class MyApp extends App {
  // Only uncomment this method if you have blocking data requirements for
  // every single page in your application. This disables the ability to
  // perform automatic static optimization, causing every page in your app to
  // be server-side rendered.
  //
  static async getInitialProps(appContext) {
    // calls page's `getInitialProps` and fills `appProps.pageProps`
    const appProps = await App.getInitialProps(appContext);

    return { ...appProps };
  }

  render() {
    const { Component, pageProps } = this.props;
    return <Component {...pageProps} />;
  }
}

export default MyApp;

@codinronan , for my case, this was the ultimate solution for this issue. Before I've tried to add only a default empty object { publicRuntimeConfig = {} } = getConfig(); and this solution alone for some reason does not deploy and it stays hanging for about 7 minutes and no response for a successful deploy on my dashboard. The lifesaver was simply add this alternative || {} to the end.

Was this page helpful?
0 / 5 - 0 ratings