[vercel/next.js]部署到 Vercel 时,使用另一个路由重写“/”时,中间件 NextResponse.rewrite 404ing

2024-05-10 589 views
6
运行next info(从版本 12.0.8 及更高版本可用)
Operating System:
  Platform: linux
  Arch: x64
  Version: #1 SMP Mon Oct 18 19:27:44 UTC 2021
Binaries:
  Node: 14.18.0
  npm: 6.14.15
  Yarn: 1.22.17
  pnpm: 6.24.4
Relevant packages:
  next: 12.0.8-canary.18
  react: 17.0.2
  react-dom: 17.0.2
您使用的 Next.js 版本是什么?

12.0.8-canary.18

您使用的 Node.js 版本是什么?

14.18.0

你使用的是什么浏览器?

火狐浏览器

描述错误

使用中间件,NextResponse.rewrite 无法使用另一个现有路由正确重写路由。这样做会导致 404。

我正在使用 i18n,但https://github.com/vercel/next.js/pull/31174无法修复它。

似乎与https://github.com/vercel/next.js/issues/30749有关

这是我的中间件文件:

import { NextRequest, NextResponse } from "next/server";
import { CONSTANTS } from "src/utils/constants";

const ONE_YEAR_IN_MS = 3600 * 365 * 24 * 1000;

export function middleware(req: NextRequest) {
  const expires = new Date(Date.now() + ONE_YEAR_IN_MS);
  const initialVariant = req.cookies[CONSTANTS.RH_HOME_PAGE_TEST_COOKIE];

  // Redirect direct traffic to our test variant
  if (
    (!initialVariant ||
      initialVariant !== CONSTANTS.RH_HOME_PAGE_VARIANT_1_SLUG) &&
    req.nextUrl.pathname === `/${CONSTANTS.RH_HOME_PAGE_VARIANT_1_SLUG}`
  ) {
    return NextResponse.redirect(CONSTANTS.HOME_ROUTE);
  }

  // Home page AB testing
  if (req.nextUrl.pathname === CONSTANTS.HOME_ROUTE) {
    let variant = initialVariant;

    // Determine the variant via a random split
    if (!variant) {
      const split: boolean = Math.random() >= 0.5;

      variant = split
        ? CONSTANTS.RH_HOME_PAGE_VARIANT_1_SLUG
        : CONSTANTS.RH_HOME_PAGE_ORIGINAL;
    }

    const res =
      variant === CONSTANTS.RH_HOME_PAGE_ORIGINAL
        ? NextResponse.next()
        : NextResponse.redirect(`/${variant}`, 307); // Temporary redirect

    if (initialVariant !== variant) {
      res.cookie(CONSTANTS.RH_HOME_PAGE_TEST_COOKIE, variant, { expires });
    }

    return res;
  } else {
    return NextResponse.next();
  }
}

我的下一个配置:

module.exports = {
  productionBrowserSourceMaps: true,
  outputFileTracing: false,
  images: {
    domains: [
      "images.ctfassets.net",
      "assets.ctfassets.net",
      "videos.ctfassets.net",
      "assets.zappyride.com",
    ],
  },
  i18n: {
    locales: ["en", "es"],
    defaultLocale: "en",
  },
  env: {},
  webpack: (config, options) => {
    if (!options.isServer) {
      config.resolve.alias["@sentry/node"] = "@sentry/react";
    }

    // Only add sentry webpack plugin when sentry DSN is defined and the
    // app is being deployed (NODE_ENV=production when making an optimized build
    // for a deployed environment).
    if (
      process.env.NEXT_PUBLIC_SENTRY_DSN &&
      process.env.NODE_ENV === "production"
    ) {
      config.plugins.push(
        new SentryWebpackPlugin({
          include: ".next",
          ignore: ["node_modules"],
          urlPrefix: "~/_next",
          release: process.env.VERCEL_GITHUB_COMMIT_SHA,
        })
      );
    }

    config.resolve.alias = {
      ...config.resolve.alias,
      "@mui/styled-engine": "@mui/styled-engine-sc",
    };

    return config;
  },
  async redirects() {
    if (process.env.ENVIRONMENT === "production") {
      return [...productionRedirects, ...sharedRedirects];
    } else {
      return [...sharedRedirects];
    }
  },
  async headers() {
    return [
      {
        source: "/",
        headers: [
          {
            key: "Cache-Control",
            value: "s-maxage=1, stale-while-revalidate",
          },
          ...securityHeaders,
        ],
      },
      {
        source: "/:path*",
        headers: [
          {
            key: "Cache-Control",
            value: "s-maxage=1, stale-while-revalidate",
          },
          ...securityHeaders,
        ],
      },
      {
        source: "/fonts/Averta/(.*)",
        headers: [
          {
            key: "Cache-Control",
            value: "public, max-age=31536000, stale-while-revalidate",
          },
          ...securityHeaders,
        ],
      },
      {
        source: "/fonts/fontface.css",
        headers: [
          {
            key: "Cache-Control",
            value: "public, max-age=31536000, stale-while-revalidate",
          },
          ...securityHeaders,
        ],
      },
    ];
  },
};
预期行为

重写应该像在 Vercel 上本地那样进行,而不是 404。

回答

4

更改if (req.url === "/")if (req.nextUrl.pathname === "/")似乎已经纠正了这个问题。

3

现在,当我尝试用另一条路由覆盖主页 url“/”并从 Vercel 访问它时,出现 404 错误和错误。同样,本地一切正常:

图像

9

从控制台的错误来看,它可能与#32549重复

8

@balazsorban44 看起来很相似。这是我更新的中间件:

import { NextRequest, NextResponse } from "next/server";
import { CONSTANTS } from "src/utils/constants";

const ONE_YEAR_IN_MS = 3600 * 365 * 24 * 1000;

export function middleware(req: NextRequest) {
  const expires = new Date(Date.now() + ONE_YEAR_IN_MS);

  // Home page AB testing
  if (req.nextUrl.pathname === "/") {
    let variant = req.cookies[CONSTANTS.RH_HOME_PAGE_TEST_COOKIE];

    // Determine the variant via a random split
    if (!variant) {
      const split: boolean = Math.random() >= 0.5; // 50/50 chance

      variant = split
        ? CONSTANTS.RH_HOME_PAGE_VARIANT_1_ROUTE
        : CONSTANTS.RH_HOME_PAGE_ORIGINAL_ROUTE;
    }

    // If variant is not original, redefine route to variant
    const route =
      variant === CONSTANTS.RH_HOME_PAGE_VARIANT_1_ROUTE
        ? `/${CONSTANTS.RH_HOME_PAGE_VARIANT_1_ROUTE}`
        : "/";

    const res = NextResponse.rewrite(route);

    res.cookie(CONSTANTS.RH_HOME_PAGE_TEST_COOKIE, variant, { expires });

    return res;
  } else {
    return NextResponse.next();
  }
}

本质上,我试图使用原始主路由(“/”)和变体(“/variant-home-refresh”)执行 AB 测试。如果我直接导​​航到该路线,我可以看到该变体,但是任何时候重写原始路线时,主页 404 和任何指向“/”的链接都无法在整个站点(徽标等)中工作。

我绝对认为这可能与 i18n 有关。

9

只是为了跟进。NextResponse.redirect()在本地和 Vercel 上均按预期工作,没有任何问题。显然,这需要注意在 url 中包含测试变体 slug,但我们现在将使用它,直到rewrite它正常工作为止。

为那些来此的人更新了中间件:

import { NextRequest, NextResponse } from "next/server";
import { CONSTANTS } from "src/utils/constants";

const ONE_YEAR_IN_MS = 3600 * 365 * 24 * 1000;

export function middleware(req: NextRequest) {
  const expires = new Date(Date.now() + ONE_YEAR_IN_MS);
  const initialVariant = req.cookies[CONSTANTS.RH_HOME_PAGE_TEST_COOKIE];

  // Redirect direct traffic to our test variant
  if (
    (!initialVariant ||
      initialVariant !== CONSTANTS.RH_HOME_PAGE_VARIANT_1_SLUG) &&
    req.nextUrl.pathname === `/${CONSTANTS.RH_HOME_PAGE_VARIANT_1_SLUG}`
  ) {
    return NextResponse.redirect(CONSTANTS.HOME_ROUTE);
  }

  // Home page AB testing
  if (req.nextUrl.pathname === CONSTANTS.HOME_ROUTE) {
    let variant = initialVariant;

    // Determine the variant via a random split
    if (!variant) {
      const split: boolean = Math.random() >= 0.5;

      variant = split
        ? CONSTANTS.RH_HOME_PAGE_VARIANT_1_SLUG
        : CONSTANTS.RH_HOME_PAGE_ORIGINAL;
    }

    const res =
      variant === CONSTANTS.RH_HOME_PAGE_ORIGINAL
        ? NextResponse.next()
        : NextResponse.redirect(`/${variant}`, 307); // Temporary redirect

    if (initialVariant !== variant) {
      res.cookie(CONSTANTS.RH_HOME_PAGE_TEST_COOKIE, variant, { expires });
    }

    return res;
  } else {
    return NextResponse.next();
  }
}
1

您可以添加已部署的最小复制@wadehammes 吗? :祈祷:澄清一下,这只是部署时的问题,但在本地有效?

9

我认为我短期内不会有时间这样做,但会尝试。

3

是的,在本地工作,而不是部署。

堆栈是最新的 接下来,i18n 配置了两个语言环境,Contentful CMS。重定向就像一个魅力,重写会导致 404。

3

我面临着同样的问题。重定向有效,重写会导致 404。仅当部署到 vercel 时才会出现此问题。

9

不确定这是否与我们在使用 next/link 导航客户端时遇到重写无法执行任何操作有关 - 将 next.js 升级到 12.0.10 并将我们的重写逻辑转换为中间件,但仍然遇到相同的问题。

客户端导航无法正确重写,而刷新页面将触发重写 - 在这两种情况下,中间件都会被触发并NextResponse.rewrite(...)从中返回

6

我遇到了同样的问题,仅在 Vercel 上部署时出现 404 页面,问题是我没有将 basePath 传递给函数rewrite,解决方案是使用类似的方法

import { NextRequest, NextResponse } from 'next/server'

export default function middleware(req: NextRequest) {
    const { pathname, basePath } = req.nextUrl

    const hostname = req.headers.get('host')

    // does not work
    // `/_hosts/${hostname}${pathname}`

    return NextResponse.rewrite(
        new URL(
            `${basePath}/_hosts/${hostname}${pathname}`,
            req.nextUrl.origin,
        ).toString(),
    )
}

使用 i18n 时可能存在同样的问题,您需要手动传递基本路径

PS:我制作了一个完整的回购协议,并在此提供了一个示例

2

我遇到了同样的问题,仅在 Vercel 上部署时出现 404 页面,问题是我没有将 basePath 传递给函数rewrite,解决方案是使用类似的方法


import { NextRequest, NextResponse } from 'next/server'

export default function middleware(req: NextRequest) {

    const { pathname, basePath } = req.nextUrl

    const hostname = req.headers.get('host')

    // does not work

    // `/_hosts/${hostname}${pathname}`

    return NextResponse.rewrite(

        new URL(

            `${basePath}/_hosts/${hostname}${pathname}`,

            req.nextUrl.origin,

        ).toString(),

    )

}

使用 i18n 时可能存在同样的问题,您需要手动传递基本路径

PS:我制作了一个完整的回购协议,并在此提供了一个示例

不幸的是,这对我不起作用。使用上面的方法仍然得到 404。

8

@wadehammes你必须像这样将语言环境添加到路径中

return NextResponse.rewrite(
        new URL(
            `${basePath}/${locale}/_hosts/${hostname}${pathname}`,
            req.nextUrl.origin,
        ).toString(),
    )

如果不使用的话可以跳过basePath

我在这里做了一个工作示例(语言环境分支)

6

我遇到了同样的 404 问题,这是因为我没有像 @remorses 提到的那样将语言环境放入路径名中。例如,

  • /a/123 -> 404
  • /en/a123 -> 有效

找到这个解决方案非常困难。至少在文档中提及这一点可能更好。 @史蒂文-泰伊

5

我遇到了同样的 404 问题,这是因为我没有像 @remorses 提到的那样将语言环境放入路径名中。例如,

* /a/123 -> 404

* /en/a123 -> works

找到这个解决方案非常困难。至少在文档中提及这一点可能更好。 @史蒂文-泰伊

可以确认这有效,谢谢@youminkim:

NextResponse.rewrite(
   new URL(
      `${basePath}/${locale}/`,
      origin
   ).toString()
);
4

大家好! ?

遇到同样的问题。中间件在本地工作,但一旦应用程序部署到 vercel,我就会不断收到重写页面的 404 错误。

我设法为该问题创建了一个存储库,可以在这里找到: https://github.com/pekac/next-middleware

此外,迷你应用程序已部署在这里: https://next-middleware-pink.vercel.app/

如果您对存储库有任何疑问 - 请随时与我们联系!

希望这可以帮助 ?

2

@wadehammes你必须像这样将语言环境添加到路径中

return NextResponse.rewrite(
        new URL(
            `${basePath}/${locale}/_hosts/${hostname}${pathname}`,
            req.nextUrl.origin,
        ).toString(),
    )

如果不使用的话可以跳过basePath

我在这里做了一个工作示例(语言环境分支)

不幸的是不为我工作

6

我们首次使用 docker 容器发布的网站也遇到了同样的问题。该网站在 Docker 部署上运行良好:https://nextjs-azalp-pltwrlrbza-ew.a.run.app/tuinhout。在本地工作正常,但部署到 Vercel 时同一页面会出现 404。客户端路由工作正常,但直接在 Vercel 部署上访问 URL 则不行:https://nextjs-azalp-9rpw5y8um-afosto.vercel.app/tuinhout

我们将此 slug 重定向到“/collection/[...slug]”,因为我们想要使用之前设置中现有的 URL。为什么这不起作用?

// slug: request.nextUrl.pathname (in this example it will be '/tuinhout')
// type: 'collection' (fetched from api or redis database)
const url = request.nextUrl.clone();
url.pathname = `${type}${slug}`;
return NextResponse.rewrite(url);
6

你好!我们一直致力于改进中间件的开发体验和 API,并且我一直在使用刚刚发布的最新金丝雀测试此示例,以确保这种情况不再发生。这个问题似乎有一些重复,所以我还将分享一些可能对这些问题有用的上下文。

首先,请注意,中间件旨在为用户提供最大的自由度,因此有些事情我们不能隐含,例如保留原始语言环境,因为我们不知道用户是否试图删除区域设置或保留它。再加上我们在调用中间件时需要绝对 URL 的事实意味着用户必须始终使用完整 URL 来表达重写。为了让事情变得更方便,我们在NextRequest一个原语中提供了NextURL解析根路径、当前区域设置等的功能。

从 OP 代码中,我了解到其目的是保留区域设置,因此NextResponse.rewrite('/')不再起作用,因为提供的 URL 必须是绝对的,如果是绝对的,您将始终重写为默认区域设置,而不保留当前的请求区域设置。实现你想要的方法是通过变异request.nextUrl.pathname然后字符串化对象。该代码看起来类似于:

import { NextRequest, NextResponse } from "next/server";

const CONSTANTS = {
  RH_HOME_PAGE_TEST_COOKIE: 'ab-test-cookie',
  RH_HOME_PAGE_VARIANT_1_ROUTE: 'index-a',
  RH_HOME_PAGE_ORIGINAL_ROUTE: 'index'
}

export function middleware(req: NextRequest) {
  // Home page AB testing
  if (req.nextUrl.pathname === "/") {
    let variant = req.cookies.get(CONSTANTS.RH_HOME_PAGE_TEST_COOKIE);

    // Determine the variant via a random split
    if (!variant) {
      variant = Math.random() >= 0.5 // 50/50 chance
        ? CONSTANTS.RH_HOME_PAGE_VARIANT_1_ROUTE
        : CONSTANTS.RH_HOME_PAGE_ORIGINAL_ROUTE;
    }

    // If variant is not original, redefine route to variant
    req.nextUrl.pathname = variant === CONSTANTS.RH_HOME_PAGE_VARIANT_1_ROUTE
      ? `/${CONSTANTS.RH_HOME_PAGE_VARIANT_1_ROUTE}`
      : "/"

    const res = NextResponse.rewrite(req.nextUrl);
    res.cookies.set(CONSTANTS.RH_HOME_PAGE_TEST_COOKIE, variant, {
      expires: new Date(Date.now() + 3600 * 365 * 24 * 1000)
    })

    return res;
  }
}

请注意,重写目标将String(req.nextUrl)遵循相同的主机、根路径、区域设置等,因为我们唯一要更改的是路径名。此示例还将 cookie API 更新为最新。

我已经在本地和生产环境中对此进行了测试,效果很好。尝试过:

  • 将页面从隐身模式转移到不同的区域设置,这样我就可以 50% 随机获得不同的版本。
  • /about从根页面导航到/,然后从关于页面返回到根。
  • 与以前相同,但使用特定的区域设置/es/about/es.

Vercel、服务器和开发模式下一切正常。如果您仍然可以使用最新的金丝雀重现该问题,请重新打开?