Open bitttttten opened 2 months ago
@bitttttten can you share the next.config file and the file that it's using the imports you mentioned?
@mauroaccornero sure!
// redis.mjs
import { CacheHandler } from '@neshca/cache-handler'
import createLruHandler from '@neshca/cache-handler/local-lru'
import createRedisHandler from '@neshca/cache-handler/redis-stack'
import invariant from 'invariant'
import { createClient } from 'redis'
const REDIS_URL = process.env.REDIS_URL
invariant(REDIS_URL, 'REDIS_URL is required inside redis.mjs')
CacheHandler.onCreation(async () => {
let client
try {
client = createClient({
url: REDIS_URL,
})
// Redis won't work without error handling. https://github.com/redis/node-redis?tab=readme-ov-file#events
client.on('error', error => {
if (process.env.NEXT_PRIVATE_DEBUG_CACHE === '1') {
// Use logging with caution in production. Redis will flood your logs. Hide it behind a flag.
console.error('[cache-handler-redis] Redis client error:', error)
}
})
} catch (error) {
console.warn('[cache-handler-redis] Failed to create Redis client:', error)
}
if (client) {
try {
console.info('[cache-handler-redis] Connecting Redis client...')
await client.connect()
console.info('[cache-handler-redis] Redis client connected.')
} catch (error) {
console.warn('[cache-handler-redis] Failed to connect Redis client:', error)
console.warn('[cache-handler-redis] Disconnecting the Redis client...')
client
.disconnect()
.then(() => {
console.info('[cache-handler-redis] Redis client disconnected.')
})
.catch(() => {
console.warn(
'[cache-handler-redis] Failed to quit the Redis client after failing to connect.',
)
})
}
}
/** @type {import("@neshca/cache-handler").Handler | null} */
let handler
if (client?.isReady) {
handler = await createRedisHandler({
client,
keyPrefix: `${process.env.PROJECT_NAME || 'next'}:`,
timeoutMs: 1000,
})
} else {
handler = createLruHandler()
console.warn(
'[cache-handler-redis] Falling back to LRU handler because Redis client is not available.',
)
}
return {
handlers: [handler],
}
})
export default CacheHandler
Our next.config.mjs looks like
import { generateConfig } from "next-configs/base.mjs";
import nextIntl from "next-intl/plugin";
import * as Sentry from "@sentry/nextjs";
import merge from "lodash.merge";
import bundleAnalyzer from '@next/bundle-analyzer'
import { getAssetPrefix, hasAssetPrefix } from '@/utils/asset-prefix.mjs'
function generateConfig(userConfig = {}) {
const assetPrefix = hasAssetPrefix() ? getAssetPrefix() : undefined;
/**
* @type {NextConfig}
*/
const base = {
logging: {
fetches: {
fullUrl: true,
},
},
assetPrefix,
reactStrictMode: true,
typescript: {
ignoreBuildErrors: true,
},
eslint: {
ignoreDuringBuilds: true,
},
trailingSlash: true,
transpilePackages: [],
experimental: {
instrumentationHook: true,
optimizePackageImports: [
"@radix-ui/primitive",
"@radix-ui/react-avatar",
"@radix-ui/react-tooltip",
"@radix-ui/react-dialog",
"@radix-ui/react-popover",
"@radix-ui/react-select",
"@radix-ui/react-slider",
"@radix-ui/react-switch",
"@radix-ui/react-radio-group",
"@radix-ui/react-toolbar",
"@radix-ui/react-tooltip",
],
},
};
const cacheHandler = {
cacheHandler: require.resolve("./redis.mjs"),
cacheMaxMemorySize: 0,
};
const config = merge(base, cacheHandler, userConfig);
if (process.env.ANALYZE === "true") {
const withBundleAnalyzer = bundleAnalyzer({
openAnalyzer: true,
});
return withBundleAnalyzer(config);
}
return config;
}
const base = generateConfig({
images: {
loader: "custom",
loaderFile: "./custom-image-loader.ts",
remotePatterns: [],
},
async rewrites() {
return {
beforeFiles: [
// some redirects
],
};
},
async headers() {
return [
{
// adds some custom headers
},
];
},
});
const withNextIntl = nextIntl("./i18n.ts");
const sentryWebpackPluginOptions = {
org: process.env.SENTRY_ORG,
project: process.env.SENTRY_PROJECT,
authToken: process.env.SENTRY_AUTH_TOKEN,
silent: true,
hideSourceMaps: false,
disableLogger: true,
sourceMaps: {
disable: true,
},
unstable_sentryWebpackPluginOptions: {
applicationKey: "*****",
},
};
export default Sentry.withSentryConfig(
withNextIntl(base),
sentryWebpackPluginOptions
);
Happy to share more info if you need.
@bitttttten thanks for the files.
if you run the build command on your machine do you get any error?
npm run build
It's weird to me the require.resolve used like that in a mjs file.
For a mjs file to use require I normally do something like this:
import { createRequire } from "node:module";
const require = createRequire(import.meta.url);
if you want you can check this example
I suggest to avoid using the cache handler when not in production, you can do that in your next.config file
cacheHandler:
process.env.NODE_ENV === "production"
? require.resolve("./redis.mjs")
: undefined
another suggestion is to not use redis cache when building, you can add an if before the try catch in your redis.mjs file
import { PHASE_PRODUCTION_BUILD } from "next/constants.js";
if (PHASE_PRODUCTION_BUILD !== process.env.NEXT_PHASE) {
(...)
}
maybe try to run the build and start command on your machine to see if you get any error
Ah sorry I am stripping a bunch of things out as they are sensitive to the project. Indeed we have require resolve already:
import { createRequire } from 'node:module'
const require = createRequire(import.meta.url)
const cacheHandler = {
cacheHandler:
process.env.REDIS_HOSTNAME && process.env.REDIS_PASSWORD
? require.resolve('./cache-handler-redis.mjs')
: require.resolve('./cache-handler-noop.js'),
cacheMaxMemorySize: 0,
}
and now we have this.
I looked into disabling the handler in the production build phase already like
// redis.mjs
class CacheHandler {
///
}
const cache = new Map()
class CacheHandlerMemory {
async get(key) {
return cache.get(key)
}
async set(key, data, ctx) {
cache.set(key, {
value: data,
lastModified: Date.now(),
tags: ctx.tags,
})
}
async revalidateTag(tag) {
for (let [key, value] of cache) {
if (value.tags.includes(tag)) {
cache.delete(key)
}
}
}
}
const loadInMemory = process.env.NEXT_PHASE === 'phase-production-build'
console.log(`Cache handler: ${loadInMemory ? 'Memory' : 'Redis'}`)
export default loadInMemory ? CacheHandlerMemory : CacheHandler
And still no bueno yet. I don't get any errors, the nextjs app compiles, it just fails to load the css when starting. If there is an error, it's getting swallowed somewhere..
Maybe you can try to replace CacheHandlerMemory with undefined. Just to skip any additional code running during the build and focus on the main cacheHandler. Maybe try to run the build and start with NEXT_PRIVATE_DEBUG_CACHE=1 to see what the cacheHandler is doing and when. Another option could be to use the example cacheHandler from next.js repository and see if you get some new error or warning.
We tried the example cacheHandler from the repo and running in the same problem. What we have done is disable the cache handler on our CI environments and locally, which is where the problem was. When we build on Vercel or inside a Dockerfile, curiously this issue is not there. I don't have much more time to debug it since I've spent maybe 8 hrs trying different combinations and debugging. And since we can disable them on CI and locally, we fingers crossed no longer see the issue. If it comes up i'll look into NEXT_PRIVATE_DEBUG_CACHE 🥳 thanks for the tip!
@bitttttten hello! Does your app live in a monorepo?
yes!
I've had issues with assets in a monorepo in the past. Please ensure that the problem isn't caused by misconfigured tracing. Refer to this Next.js documentation on outputFileTracingRoot
at https://nextjs.org/docs/app/api-reference/next-config-js/output#caveats.
oh interesting, that was actually my next thing to attempt to look into! when i manage to implement the outputFileTracingRoot and see it's affect, i will let you know. thanks
@bitttttten hello! Did the outputFileTracingRoot
option help?
I am running into similar issues, is there a recommended fix or the known root-cause of why this happens?
I am also seeing scenarios where redeploying changes seem to make the pages go 404 and those never get revalidated unless, I manually revalidate each page.
@whizzzkid hi! Which Next.js Router do you use in your project?
@bitttttten thanks for the files.
if you run the build command on your machine do you get any error?
npm run build
It's weird to me the require.resolve used like that in a mjs file.
For a mjs file to use require I normally do something like this:
import { createRequire } from "node:module"; const require = createRequire(import.meta.url);
if you want you can check this example
I suggest to avoid using the cache handler when not in production, you can do that in your next.config file
cacheHandler: process.env.NODE_ENV === "production" ? require.resolve("./redis.mjs") : undefined
another suggestion is to not use redis cache when building, you can add an if before the try catch in your redis.mjs file
import { PHASE_PRODUCTION_BUILD } from "next/constants.js"; if (PHASE_PRODUCTION_BUILD !== process.env.NEXT_PHASE) { (...) }
maybe try to run the build and start command on your machine to see if you get any error
@mauroaccornero the link to your example is no longer available:
@JSONRice it's public now
Am facing this issue as well, when the application is redeployed, it is using the previous cache version of a static page which reference assets from the previous deployment. Is there away to ensure the cache is purged or is some configuration missing on my end? Would this config cause any issue, it is recommended by Next.js cacheMaxMemorySize: isProd ? 0 : undefined
Brief Description of the Bug
When using
We see that our static assets like JS and CSS files sometimes 404.
So not all, but some.
Severity Maybe Major?
Frequency of Occurrence Always
Environment:
It happens on CircleCI and local on M3 and M1.
Dependencies and Versions next@14.2.3 @neshca/cache-handler@1.5.1
Attempted Solutions or Workarounds None so far, we removed the package and continuing using our own redis cache handler.
Impact of the Bug Critical, we cannot get Next.JS assets loaded.
Additional context None, but happy to provide more!