Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Using redis-stack & lru makes static assets 404 #703

Open
bitttttten opened this issue Aug 17, 2024 · 16 comments
Open

Using redis-stack & lru makes static assets 404 #703

bitttttten opened this issue Aug 17, 2024 · 16 comments
Labels
bug Something isn't working maybe fixed Waiting for response

Comments

@bitttttten
Copy link

Brief Description of the Bug

When using

import createLruHandler from '@neshca/cache-handler/local-lru'
import createRedisHandler from '@neshca/cache-handler/redis-stack'

We see that our static assets like JS and CSS files sometimes 404.

Screenshot 2024-08-17 at 18 37 35

Screenshot 2024-08-17 at 18 36 03

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
[email protected]
@neshca/[email protected]

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!

@bitttttten bitttttten added the bug Something isn't working label Aug 17, 2024
@mauroaccornero
Copy link

@bitttttten can you share the next.config file and the file that it's using the imports you mentioned?

@bitttttten
Copy link
Author

bitttttten commented Aug 19, 2024

@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.

@mauroaccornero
Copy link

@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

@bitttttten
Copy link
Author

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..

@mauroaccornero
Copy link

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.

@bitttttten
Copy link
Author

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!

@better-salmon
Copy link
Contributor

@bitttttten hello! Does your app live in a monorepo?

@bitttttten
Copy link
Author

yes!

@better-salmon
Copy link
Contributor

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.

@bitttttten
Copy link
Author

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

@better-salmon
Copy link
Contributor

@bitttttten hello! Did the outputFileTracingRoot option help?

@better-salmon better-salmon added the maybe fixed Waiting for response label Sep 5, 2024
@whizzzkid
Copy link

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.

@better-salmon
Copy link
Contributor

@whizzzkid hi! Which Next.js Router do you use in your project?

@JSONRice
Copy link

JSONRice commented Oct 2, 2024

@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:

@mauroaccornero
Copy link

@JSONRice it's public now

@ramonmalcolm10
Copy link

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working maybe fixed Waiting for response
Projects
None yet
Development

No branches or pull requests

6 participants