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.