Next.js Discord

Discord Forum

API request not being made in production build

Unanswered
Giant Angora posted this in #help-forum
Open in Discord
Giant AngoraOP
I have a basic Next.js a single page on the root which in order to render some images needs to make an API request to retrieve the image URLs.

This works during development but when I build the app and run it in an environment where IMAGE_API_URL is set correctly.

The result is always No images found!

What gives?

I've even attached a Node.js debugger to the running server process and I can see with the transpiled JS code it never gets to await fetch(...) even though process.env.IMAGE_API_URL is set and available.

import Image from 'next/image';

async function getImages() {
  const IMAGE_API_URL = process.env.IMAGE_API_URL;
  // This was added because otherwise the production build would fail due to trying to make a request to this API which is not available at build time.
  if (!IMAGE_API_URL) {
    return [];
  }
  const res = await fetch(`${IMAGE_API_URL}/api/images`);
  return res.json();
}

export default async function Home() {
  const images = await getImages();
  return (
    <main>
      {images.length === 0 && <h1>No images found!</h1>}
      {images.map(image => (
        <Image key={image.id} alt={image.alt} src={image.src} width={image.width} height={image.height}/>
      )}
    </main>
  );
}

92 Replies

@Ray what is the value of `IMAGE_API_URL`?
Giant AngoraOP
At what point?
@Giant Angora At what point?
it is an external api?
Giant AngoraOP
or the url for next?
Giant AngoraOP
It's an "external" API
is /api/images a route handler?
ah ok
Giant AngoraOP
By external I mean not part of the Next.js project but still implemented by me
import { unstable_noStore as noStore } from 'next/cache'

async function getImages() {
  noStore()
  const IMAGE_API_URL = process.env.IMAGE_API_URL;
  // This was added because otherwise the production build would fail due to trying to make a request to this API which is not available at build time.
  if (!IMAGE_API_URL) {
    return [];
  }
  const res = await fetch(`${IMAGE_API_URL}/api/images`);
  return res.json();
}

try this
@Ray is your page static generated?
Giant AngoraOP
So I ended up solving it by forcing it not to be static.
export const dynamic = `force-dynamic`;

However, now I've got a new issue where attempting to load the image results in the following error response.
$ curl -i https://my-site.com/_next/image?url...
HTTP/2 400 
date: Fri, 02 Feb 2024 23:39:46 GMT
strict-transport-security: max-age=31536000; includeSubDomains

"url" parameter is not allowed
what does the src src={image.src} look like in the dom?
Giant AngoraOP
I have my next.config.js as so...
const url = require('node:url');

/** @type {import('next').NextConfig} */
module.exports = {
  output: 'standalone',
  images: {
    domains: [
      'placekitten.com',
      url.parse(process.env.AWS_ENDPOINT_URL || 'http://minio.localhost')
        .hostname,
      url.parse(process.env.IMGPROXY_API_URL || 'http://imgproxy.localhost')
        .hostname,
    ],
  },
};
I assume it's failing because the domain in the ?url=... parameter isn't allowed?
what version of next are you using?
Giant AngoraOP
14.0.3
try adding your domain like this in the next.config.js
Giant AngoraOP
I don't know the domain until runtime
That's why I'm setting it from the environment variable values.
I mean use remotePatterns instead of domains
Giant AngoraOP
Right
But will it use the value of process.env.AWS_ENDPOINT_URL?
are you setting the variable after build?
Giant AngoraOP
Yes
Only at runtime
then yes, you gotta use runtime variable
Giant AngoraOP
Are those still valid in 14 / App router?
@Ray https://nextjs.org/docs/app/building-your-application/configuring/environment-variables#runtime-environment-variables
Giant AngoraOP
Yeah the environment variable doesn't need to be client-side
create a function
import { unstable_noStore as noStore } from 'next/cache'

export function getVar(variable: string) {
  noStore()
  return process.env[variable]
}
Giant AngoraOP
This is regarding the next.config.js
I think we're getting our wires crossed.
Next.js is now on the server-side making the API request and getting the image URL correctly and sending it to the client.
The client is making a request for the image URL sent by the backend.
well I think next.config.js is runtime
try to copy it to the server
Giant AngoraOP
That request to /_next/image?url=... is failing I think because the domain of the URL is not whitelisted in the next.config.js
I suspect because after the build next.config.js is not longer lookup the config from the process.env
>This feature is deprecated. We recommend using environment variables instead, which also can support reading runtime values.
where do I copy it the standalone build should have included the next.config.js that was there at build time no?
@Giant Angora That request to `/_next/image?url=...` is failing I think because the domain of the URL is not whitelisted in the `next.config.js`
well if the domain is not whitelist it will say it instead of url parameter isn't allowed
Giant AngoraOP
/app # tree .next/
.next/
├── BUILD_ID
├── app-build-manifest.json
├── app-path-routes-manifest.json
├── build-manifest.json
├── package.json
├── prerender-manifest.js
├── prerender-manifest.json
├── react-loadable-manifest.json
├── required-server-files.json
├── routes-manifest.json
├── server
│   ├── app
│   │   ├── _not-found.html
│   │   ├── _not-found.js
│   │   ├── _not-found.js.nft.json
│   │   ├── _not-found.meta
│   │   ├── _not-found.rsc
│   │   ├── _not-found_client-reference-manifest.js
│   │   ├── api
│   │   │   └── images
│   │   │       ├── route.js
│   │   │       └── route.js.nft.json
│   │   ├── favicon.ico
│   │   │   ├── route.js
│   │   │   └── route.js.nft.json
│   │   ├── favicon.ico.body
│   │   ├── favicon.ico.meta
│   │   ├── index.prefetch.rsc
│   │   ├── page.js
│   │   ├── page.js.nft.json
│   │   ├── page_client-reference-manifest.js
│   │   ├── tos
│   │   │   ├── page.js
│   │   │   ├── page.js.nft.json
│   │   │   └── page_client-reference-manifest.js
│   │   ├── tos.html
│   │   ├── tos.meta
│   │   └── tos.rsc
│   ├── app-paths-manifest.json
│   ├── chunks
│   │   ├── 227.js
│   │   ├── 271.js
│   │   ├── 687.js
│   │   ├── 732.js
│   │   └── font-manifest.json
│   ├── font-manifest.json
│   ├── middleware-build-manifest.js
│   ├── middleware-manifest.json
│   ├── middleware-react-loadable-manifest.js
│   ├── next-font-manifest.js
│   ├── next-font-manifest.json
│   ├── pages
│   │   ├── 404.html
│   │   ├── 500.html
│   │   ├── _app.js
│   │   ├── _app.js.nft.json
│   │   ├── _document.js
│   │   ├── _document.js.nft.json
│   │   ├── _error.js
│   │   └── _error.js.nft.json
│   ├── pages-manifest.json
│   ├── server-reference-manifest.js
│   ├── server-reference-manifest.json
│   └── webpack-runtime.js
└── static

16 directories, 81 files
@Ray well if the domain is not whitelist it will say it instead of url parameter isn't allowed
Giant AngoraOP
Okay so what's the problem then? Why is the client unable to make a request for the image?
When if I load that URL from the ?url=... parameter in the browser it loads fine
Giant AngoraOP
>2020
Seems like a major oversight
ok i have no idea
Giant AngoraOP
Should it go in the root next to the server.js created by output: 'standalone'?
Giant AngoraOP
I'll connect a debugger and verify this is the issue
So it appears the values that would have been set in next.config.js at build time are there.
But not the value of the process.env vars that should have overridden it at runtime.
Giant AngoraOP
So it seems the config is embedded in the server.js file as a static JS object.
and none of the process.env carried over
So why doesn't Next.js copy over the entire expression I put in next.config.js ?
const url = require('node:url');

/** @type {import('next').NextConfig} */
module.exports = {
  output: 'standalone',
  images: {
    domains: [
      'placekitten.com',
      url.parse(process.env.AWS_ENDPOINT_URL || 'http://minio.localhost')
        .hostname,
      url.parse(process.env.IMGPROXY_API_URL || 'http://imgproxy.localhost')
        .hostname,
    ],
  },
};
Where is this fabled 'runtime' config.
@Giant Angora So why doesn't Next.js copy over the entire expression I put in `next.config.js` ?
look like it copy the variable when you build
Giant AngoraOP
Yeah exactly along with whatever defaults there are.
If I use this...
const url = require('node:url');

/** @type {import('next').NextConfig} */
module.exports = {
  output: 'standalone',
  images: {
    domains: [
      'placekitten.com',
      process.env.AWS_ENDPOINT_URL,
      process.env.IMGPROXY_API_URL,
    ],
  },
};


The build fails with...
0.697 > next build
0.697 
1.069 Invalid next.config.js options detected: 
1.069     "images.domains[1]" is missing, expected string
1.069     "images.domains[2]" is missing, expected string
1.069 See more info here: https://nextjs.org/docs/messages/invalid-next-config
1.075  ELIFECYCLE  Command failed with exit code 1
could you create a CNAME and add it to next.config.js
Giant AngoraOP
I tried this but it's still not using process.env in the server.js of the production build.

const url = require('node:url');

const { PHASE_DEVELOPMENT_SERVER, PHASE_PRODUCTION_SERVER } = require('next/constants')

/** @type {import('next').NextConfig} */
module.exports = (phase) => {
  switch (phase) {
    case PHASE_PRODUCTION_SERVER:
      return {
        output: 'standalone',
        images: {
          domains: [
            `placekitten.com`,
            url.parse(process.env.AWS_ENDPOINT_URL).hostname,
            url.parse(process.env.IMGPROXY_API_URL).hostname,
          ]
        }
      }
    case PHASE_DEVELOPMENT_SERVER:
    default:
      return {
        output: 'standalone',
        images: {
          domains: [
            'placekitten.com',
            'minio.localhost',
            'imgproxy.localhost',
          ]
        }
      };
    }
}
Giant AngoraOP
Looks like people having to do a find and replace of their variables in an entrypoing script.
https://github.com/vercel/next.js/discussions/44628#discussioncomment-7034433
Bonkers
@Ray try this
Giant AngoraOP
You don't get it my dude
I don't know what it'll be until runtime
yes
Giant AngoraOP
I'm not going to create CNAMEs adhoc
On the fly
Pointing to 1000s of different domains
The point is to deploy it to an environment
and the environment sets the config
build once run anywhere
Giant AngoraOP
That's because those are used in local development
If I deploy 100 instances of this nextjs applications
and each instance has a different hostname for imgproxy or s3
then I need to be able to set that from an environment variable at runtime
Giant AngoraOP
I ended up patching the built server.js by having the following in my next.config.js.
/** @type {import('next').NextConfig} */
module.exports = {
  output: 'standalone',
  images: {
    domains: [
      'placekitten.com',
      'minio.localhost',
      'imgproxy.localhost',
      "${require('url').parse(process.env.AWS_ENDPOINT_URL).hostname}",
      "${require('url').parse(process.env.IMGPROXY_API_URL).hostname}",
    ],
  },
};

Then patching the server.js with sed after it's built to unwrap these strings.
RUN sed -i -r 's/"\$\{([^}]*)\}"/\1/g' server.js

Which turns into this in the final server.js.
// ...
  "path": "/_next/image",
  "loader": "default",
  "loaderFile": "",
  "domains": [
    "placekitten.com",
    "minio.localhost",
    "imgproxy.localhost",
    require("url").parse(process.env.AWS_ENDPOINT_URL).hostname,
    require("url").parse(process.env.IMGPROXY_API_URL).hostname,
  ],
  "disableStaticImages": false,
  "minimumCacheTTL": 60,
// ...

🤪