from errors with not-found to the next-runtime-env fork

I recently came across an interesting bug in Next.js. If on the page not-found navigate through router.push(pathname)all environment variables that we initialize through the library are lost next-runtime-env (meaning window.__ENV becomes undefined).

In the project we use next-runtime-envsince we adhere to the approach Build once, deploy many – this allows you to keep one Docker image, into which the necessary environment variables are thrown at startup. Next.js does not support this behavior out of the box, because it wants to collect env variables at the application build stage.

The bug appeared on not-found page where we have a button that allows you to create an element in one click if something is not found. The same button component is used on other pages, and here's what's interesting: on other pages router.push(pathname) works correctly, but not-found – No.

At first I thought the problem lay in next-runtime-env. The library is probably overridden when the page is refreshed, because the script that sets the variables in window.__ENVplaced in root layout. I also tried versioning Next.js, assuming that the bug was related to certain versions of the framework, but this did not yield any results. As a result, a temporary solution was to use window.location.hrefwhich prevented page refresh and helped save variables.

However, the story did not end there.


The problem has narrowed down: only the client config was lost

I dug deeper into the problem, trying to understand why the config was being lost only in the client environment. Initially I suspected router.push(pathname) – thought it was causing a full page reload, clearing window and without causing the script to be executed again from the root layout. However, this turned out not to be the case.

Trying to reproduce the bug in minimalist sandboxes, I was faced with the fact that the bug did not appear there. Then I paid attention to middleware next-intlwhich could magically influence environmental variables during navigation. During the debugging process, I noticed that in sandboxes and in the project I was getting into not-found in different ways: through notFound() and through router.push(/not-existed-pathname). This led me to even more experiments.

In the comments in my telegram blog To my attempts to find out the cause of the problem, a reasonable question was asked: “What exactly decides next-runtime-env? ” I replied that I usually use third-party libraries to manage the complexity of the project, but the problem clearly required more detailed delving into the roots.

Having saved up the energy to debug, I started testing different hypotheses:

  1. Replace the native inline script with a script from next/scriptto play around with different loading strategies.

  2. Check if this is due to the fact that starting from version 14, not-found is prepared by default on the server.

  3. Understand why a complete page refresh occurs when navigating through router.push.


There were no ends in sight

After numerous experiments, including replacing native scripts with next/script with different loading strategies, I was still unable to get the config on the page not-found through router.push. I tried to duplicate scripts on the server, played with the directive use clientbut to no avail.

At some point my enthusiasm dried up. I even deleted next-intl from the project to check if its middleware is affecting it, but this did not solve the problem.

Then I noticed that on the page not-found object present window.__next_fwhich reminded me of RSC Payload. In it I saw my script with the config, but for some reason Next.js did not hydrogenate this part of the page.

At this stage, I was already thinking about writing an RSC payload parser in order to pull out the config from there and mine it into windowbut something distracted me, and then I decided to look for what they write about this __next_f.

Interestingly, when rendering the page not-foundNext.js throws a certain error, and it reacts to all errors in the same way – it stops rendering the page, including the layout, where my configuration assignment took place. How explained in one of the threads, the problem is that Next.js stops rendering of any page when an error occurs, be it NotFoundError or another. As a result, the server stops rendering the page, including all layout elements. It is important to note that the initial HTML that the server returns does not contain specific layout elements.

It doesn't matter if the error is thrown via notFound() or through regular new Error()the route rendering stops, and the HTML returned by the server does not contain the necessary tags for the scripts working with beforeInteractive. When hydration is complete, the script is added to the HTML, but by this point the main app-bootstrap the script has already been executed and its impact is lost. Therefore the loading strategy beforeInteractive does not work on pages with errors.

However, all other loading strategies such as afterInteractive or lazyOnLoadwork correctly. This prompted me to check how loading scripts into next-runtime-envand it turned out that it is also used there beforeInteractive. I decided to open PR to the repository, especially since I already found two issues where developers encountered similar problems – loss of configuration on the client.

Good thing I'm not the only one who has encountered this. Let's see how the team reacts to the PR. I'm wondering why they didn't add support for the script loading strategy via prop strategyand is there some conceptual limitation that I'm not grasping yet. If there are no conceptual limitations, great! Otherwise, perhaps I'll make my own implementation to pass the configuration to the client without loss.


Fork and his fate

In the end, I decided to release my fork of the library next-runtime-env. It can be found on GitHub: awesome-next-runtime-env.

I described all the differences from the original in PR. Whether this fork will get further development will depend on whether they upload my PR or not.


Thus, the history of the search for a solution, starting with navigation on not-found page and ending with a library fork, turned out to be full of unexpected twists.

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *