Advanced
Workaround

WORKAROUND: Create one cache behavior per top-level file and folder in public/ (AWS specific)

As mentioned in the Asset files section, files in your app's public/ folder are static and are uploaded to the S3 bucket. And requests for these files are handled by the S3 bucket, like so:

https://my-nextjs-app.com/favicon.ico
https://my-nextjs-app.com/my-images/avatar.png

Ideally, we would create a single cache behavior that routes all requests for public/ files to the S3 bucket. Unfortunately, CloudFront does not support regex or advanced string patternss for cache behaviors (ie. /favicon.ico|my-images\/*/ ).

To work around this limitation, we create a separate cache behavior for each top-level file and folder in public/. For example, if your folder structure is:

public/
  favicon.ico
  my-images/
    avatar.png
    avatar-dark.png
  foo/
    bar.png

You would create three cache behaviors: /favicon.ico, /my-images/*, and /foo/*. Each of these behaviors points to the S3 bucket.

One thing to be aware of is that CloudFront has a default limit of 25 behaviors per distribution (opens in a new tab). If you have a lot of top-level files and folders, you may reach this limit. To avoid this, consider moving some or all files and folders into a subdirectory:

public/
  files/
    favicon.ico
    my-images/
      avatar.png
      avatar-dark.png
    foo/
      bar.png

In this case, you only need to create one cache behavior: /files/*.

Make sure to update your code accordingly to reflect the new file paths.

Alternatively, you can request an increase to the limit through AWS Support (opens in a new tab).

WORKAROUND: Set x-forwarded-host header (AWS specific)

When the server function receives a request, the host value in the Lambda request header is set to the hostname of the AWS Lambda service instead of the actual frontend hostname. This creates an issue for the server function (middleware, SSR routes, or API routes) when it needs to know the frontend host.

To work around the issue, a CloudFront function is run on Viewer Request, which sets the frontend hostname as the x-forwarded-host header. The function code looks like this:

function handler(event) {
  var request = event.request;
  request.headers["x-forwarded-host"] = request.headers.host;
  return request;
}

The server function would then sets the host header of the request to the value of the x-forwarded-host header when sending the request to the NextServer.

WORKAROUND: Set NextRequest geolocation data

When your application is hosted on Vercel, you can access a user's geolocation inside your middleware through the NextRequest object.

export function middleware(request: NextRequest) {
  request.geo.country;
  request.geo.city;
}

When your application is hosted on AWS, you can obtain the geolocation data from CloudFront request headers (opens in a new tab). However, there is no way to set this data on the NextRequest object passed to the middleware function.

To work around the issue, the NextRequest constructor is modified to initialize geolocation data from CloudFront headers, instead of using the default empty object.

- geo: init.geo || {}
+ geo: init.geo || {
+   country: this.headers("cloudfront-viewer-country"),
+   countryName: this.headers("cloudfront-viewer-country-name"),
+   region: this.headers("cloudfront-viewer-country-region"),
+   regionName: this.headers("cloudfront-viewer-country-region-name"),
+   city: this.headers("cloudfront-viewer-city"),
+   postalCode: this.headers("cloudfront-viewer-postal-code"),
+   timeZone: this.headers("cloudfront-viewer-time-zone"),
+   latitude: this.headers("cloudfront-viewer-latitude"),
+   longitude: this.headers("cloudfront-viewer-longitude"),
+   metroCode: this.headers("cloudfront-viewer-metro-code"),
+ }

CloudFront provides more detailed geolocation information, such as postal code and timezone. Here is a complete list of geo properties available in your middleware:

export function middleware(request: NextRequest) {
  // Supported by Next.js
  request.geo.country;
  request.geo.region;
  request.geo.city;
  request.geo.latitude;
  request.geo.longitude;
 
  // Also supported by OpenNext
  request.geo.countryName;
  request.geo.regionName;
  request.geo.postalCode;
  request.geo.timeZone;
  request.geo.metroCode;
}

WORKAROUND: NextServer does not set cache headers for HTML pages

As mentioned in the Server function section, the server function uses the NextServer class from Next.js' build output to handle requests. However, NextServer does not seem to set the correct Cache Control headers.

To work around the issue, the server function checks if the request is for an HTML page, and sets the Cache Control header to:

public, max-age=0, s-maxage=31536000, must-revalidate

WORKAROUND: NextServer does not set correct SWR cache headers

NextServer does not seem to set an appropriate value for the stale-while-revalidate cache header. For example, the header might look like this:

s-maxage=600 stale-while-revalidate

This prevents CloudFront from caching the stale data.

To work around the issue, the server function checks if the response includes the stale-while-revalidate header. If found, it sets the value to 30 days:

s-maxage=600 stale-while-revalidate=2592000

WORKAROUND: Set NextServer working directory (AWS specific)

Next.js recommends using process.cwd() instead of __dirname to get the app directory. For example, consider a posts folder in your app with markdown files:

pages/
posts/
  my-post.md
public/
next.config.js
package.json

You can build the file path like this:

path.join(process.cwd(), "posts", "my-post.md");

As mentioned in the Server function section, in a non-monorepo setup, the server-function bundle looks like:

.next/
node_modules/
posts/
  my-post.md    <- path is "posts/my-post.md"
index.mjs

In this case, path.join(process.cwd(), "posts", "my-post.md") resolves to the correct path.

However, when the user's app is inside a monorepo (ie. at /packages/web), the server-function bundle looks like:

packages/
  web/
    .next/
    node_modules/
    posts/
      my-post.md    <- path is "packages/web/posts/my-post.md"
    index.mjs
node_modules/
index.mjs

In this case, path.join(process.cwd(), "posts", "my-post.md") cannot be resolved.

To work around the issue, we change the working directory for the server function to where .next/ is located, ie. packages/web.

WORKAROUND: Set __NEXT_PRIVATE_PREBUNDLED_REACT to use prebundled React

For Next.js 13.2 and later versions, you need to explicitly set the __NEXT_PRIVATE_PREBUNDLED_REACT environment variable. Although this environment variable isn't documented at the time of writing, you can refer to the Next.js source code to understand its usage:

In standalone mode, we don't have separated render workers so if both app and pages are used, we need to resolve to the prebundled React to ensure the correctness of the version for app.

Require these modules with static paths to make sure they are tracked by NFT when building the app in standalone mode, as we are now conditionally aliasing them it's tricky to track them in build time.

On every request, we try to detect whether the route is using the Pages Router or the App Router. If the Pages Router is being used, we set __NEXT_PRIVATE_PREBUNDLED_REACT to undefined, which means the React version from the node_modules is used. However, if the App Router is used, __NEXT_PRIVATE_PREBUNDLED_REACT is set, and the prebundled React version is used.

WORKAROUND: 13.4.13+ breaking changes (middleware, redirect, rewrites)

Nextjs 13.4.13 refactored the middleware logic so that it no longer runs in the server handler. Instead they are executed as workers in child threads, which introduces a non-acceptable latency of ~5 seconds. In order to circumvent this issue, open-next needs to implement the middleware handler before processing the server handler ourselves.

We've introduced a custom esbuild plugin to conditionally inject and override code to properly handle the breaking changes.

The default request handler is in adapters/plugins/default.ts When open-next needs to override that implementation due to NextJs breaking compatibility, the createServerBundle in build.ts determines the proper overrides to replace the code of the default.ts file.