diff --git a/errors/missing-env-value.md b/errors/missing-env-value.md index 7c2d3865f4ef..a8d1a1fda5f2 100644 --- a/errors/missing-env-value.md +++ b/errors/missing-env-value.md @@ -2,24 +2,15 @@ #### Why This Error Occurred -One of your pages' config requested an env value that wasn't populated. +In one of your pages you attempted to access an environment value that is not provided in the environment. -```js -// pages/index.js -export const config = { - // this value isn't provided in `.env` - env: ['MISSING_KEY'], -} -``` +When accessing environment variables on the client they must be prefixed with `NEXT_PUBLIC_` to signify they are safe to be inlined for the client. -``` -// .env (notice no `MISSING_KEY` provided here) -NOTION_KEY='...' -``` +When accessing environment variables on the server in `getStaticProps`, `getServerSideProps`, or an API route the value must be provided in the environment at runtime. #### Possible Ways to Fix It -Either remove the requested env value from the page's config, populate it in your `.env` file, or manually populate it in your environment before running `next dev` or `next build`. +Either remove the code accessing the env value, populate it in your `.env` file, or manually populate it in your environment before running `next dev` or `next build`. ### Useful Links diff --git a/packages/next/build/webpack-config.ts b/packages/next/build/webpack-config.ts index c1aaed6a33ae..96ef8be35923 100644 --- a/packages/next/build/webpack-config.ts +++ b/packages/next/build/webpack-config.ts @@ -838,6 +838,27 @@ export default async function getBaseWebpackConfig( 'global.GENTLY': JSON.stringify(false), } : undefined), + // stub process.env with proxy to warn a missing value is + // being accessed + ...(config.experimental.pageEnv + ? { + 'process.env': + process.env.NODE_ENV === 'production' + ? isServer + ? 'process.env' + : '{}' + : ` + new Proxy(${isServer ? 'process.env' : '{}'}, { + get(target, prop) { + if (typeof target[prop] === 'undefined') { + console.warn(\`An environment variable (\${prop}) that was not provided in the environment was accessed.\nSee more info here: https://err.sh/next.js/missing-env-value\`) + } + return target[prop] + } + }) + `, + } + : {}), }), !isServer && new ReactLoadablePlugin({ diff --git a/test/integration/process-env-stub/next.config.js b/test/integration/process-env-stub/next.config.js new file mode 100644 index 000000000000..80ee697d9b1b --- /dev/null +++ b/test/integration/process-env-stub/next.config.js @@ -0,0 +1,5 @@ +module.exports = { + experimental: { + pageEnv: true, + }, +} diff --git a/test/integration/process-env-stub/pages/also-not-missing.js b/test/integration/process-env-stub/pages/also-not-missing.js new file mode 100644 index 000000000000..b24d59f2501b --- /dev/null +++ b/test/integration/process-env-stub/pages/also-not-missing.js @@ -0,0 +1,4 @@ +export default () => { + console.log(process.env.I_SHOULD_BE_HERE) + return

hi there 👋

+} diff --git a/test/integration/process-env-stub/pages/api/hi.js b/test/integration/process-env-stub/pages/api/hi.js new file mode 100644 index 000000000000..d8a9c2fcc36c --- /dev/null +++ b/test/integration/process-env-stub/pages/api/hi.js @@ -0,0 +1,4 @@ +export default (req, res) => { + console.log(process.env.where_is_it) + res.end('done') +} diff --git a/test/integration/process-env-stub/pages/missing-gsp.js b/test/integration/process-env-stub/pages/missing-gsp.js new file mode 100644 index 000000000000..23d96ed62e39 --- /dev/null +++ b/test/integration/process-env-stub/pages/missing-gsp.js @@ -0,0 +1,12 @@ +export default () => { + return

hi there 👋

+} + +export const getStaticProps = () => { + console.log(process.env.SECRET) + return { + props: { + hi: 'there', + }, + } +} diff --git a/test/integration/process-env-stub/pages/missing-gssp.js b/test/integration/process-env-stub/pages/missing-gssp.js new file mode 100644 index 000000000000..cbfd2f386553 --- /dev/null +++ b/test/integration/process-env-stub/pages/missing-gssp.js @@ -0,0 +1,12 @@ +export default () => { + return

hi there 👋

+} + +export const getServerSideProps = () => { + console.log(process.env.SECRET) + return { + props: { + hi: 'there', + }, + } +} diff --git a/test/integration/process-env-stub/pages/missing.js b/test/integration/process-env-stub/pages/missing.js new file mode 100644 index 000000000000..7432bad23e77 --- /dev/null +++ b/test/integration/process-env-stub/pages/missing.js @@ -0,0 +1,4 @@ +export default () => { + console.log(process.env.hi) + return

hi there 👋

+} diff --git a/test/integration/process-env-stub/pages/not-missing.js b/test/integration/process-env-stub/pages/not-missing.js new file mode 100644 index 000000000000..794e996c3315 --- /dev/null +++ b/test/integration/process-env-stub/pages/not-missing.js @@ -0,0 +1,4 @@ +export default () => { + console.log(process.env.NEXT_PUBLIC_HI) + return

hi there 👋

+} diff --git a/test/integration/process-env-stub/test/index.test.js b/test/integration/process-env-stub/test/index.test.js new file mode 100644 index 000000000000..b27b32817d87 --- /dev/null +++ b/test/integration/process-env-stub/test/index.test.js @@ -0,0 +1,110 @@ +/* eslint-env jest */ +/* global jasmine */ +import { + findPort, + killApp, + launchApp, + renderViaHTTP, + waitFor, +} from 'next-test-utils' +import { join } from 'path' +import webdriver from 'next-webdriver' + +jasmine.DEFAULT_TIMEOUT_INTERVAL = 1000 * 60 * 2 +const appDir = join(__dirname, '..') +let app +let stderr +let appPort + +const buildWarning = prop => + `An environment variable (${prop}) that was not provided in the environment was accessed` + +const checkMissing = async (pathname, prop, shouldWarn = false) => { + stderr = '' + await renderViaHTTP(appPort, pathname) + await waitFor(1000) + + if (shouldWarn) { + expect(stderr).toContain(buildWarning(prop)) + } else { + expect(stderr).not.toContain(buildWarning(prop)) + } +} + +const checkMissingClient = async (pathname, prop, shouldWarn = false) => { + const browser = await webdriver(appPort, '/404') + await browser.eval(`(function() { + window.warnLogs = [] + var origWarn = console.warn + + console.warn = function() { + window.warnLogs.push(arguments[0]) + origWarn.apply(this, arguments) + } + window.next.router.push("${pathname}") + })()`) + await waitFor(2000) + + const logs = await browser.eval(`window.warnLogs`) + const warning = buildWarning(prop) + const hasWarn = logs.some(log => log.includes(warning)) + + expect(hasWarn).toBe(shouldWarn) + await browser.close() +} + +describe('process.env stubbing', () => { + beforeAll(async () => { + appPort = await findPort() + app = await launchApp(appDir, appPort, { + env: { + NEXT_PUBLIC_HI: 'hi', + I_SHOULD_BE_HERE: 'hello', + }, + onStderr(msg) { + stderr += msg || '' + }, + }) + }) + afterAll(() => killApp(app)) + + describe('server side', () => { + it('should not show missing env value when its not missing public', async () => { + await checkMissing('/not-missing', 'NEXT_PUBLIC_HI') + }) + + it('should not show missing env value when its not missing runtime', async () => { + await checkMissing('/also-not-missing', 'I_SHOULD_BE_HERE') + }) + + it('should show missing env value when its missing normal', async () => { + await checkMissing('/missing', 'hi', true) + }) + + it('should show missing env value when its missing GSP', async () => { + await checkMissing('/missing-gsp', 'SECRET', true) + }) + + it('should show missing env value when its missing GSSP', async () => { + await checkMissing('/missing-gssp', 'SECRET', true) + }) + + it('should show missing env value when its missing API', async () => { + await checkMissing('/api/hi', 'where_is_it', true) + }) + }) + + describe('client side', () => { + it('should not show missing env value when its not missing public', async () => { + await checkMissingClient('/not-missing', 'NEXT_PUBLIC_HI') + }) + + it('should show missing env value when its missing runtime', async () => { + await checkMissingClient('/also-not-missing', 'I_SHOULD_BE_HERE', true) + }) + + it('should show missing env value when its missing normal', async () => { + await checkMissingClient('/missing', 'hi', true) + }) + }) +})