amannn / next-intl

🌐 Internationalization (i18n) for Next.js
https://next-intl-docs.vercel.app
MIT License
2.65k stars 237 forks source link

An unexpected redirection from a request rewrite #1473

Closed jefer94 closed 1 month ago

jefer94 commented 1 month ago

Description

I have a folder called protected to views like not found, maintenance, countdown, the idea is that folder can only be accessed using rewriting, I saw that the browser got that like a redirection, why? because next-intl middleware returns a 307, causing a redirection and rendering a 404 request, I can't change that response status

Verifications

Mandatory reproduction URL

https://capyschool.com/

Reproduction description

  1. Open capyschool.com
  2. go to capyschool.com/es
  3. go again to go to capyschool.com

Expected behaviour

That it don't redirect to /es/protected/countdown

middleware

"use server";

import { NextRequest, NextResponse } from "next/server";
import { tokenInfo } from "@/actions/auth";
import { showMaintenance } from "./flags";
// import { getLanguage } from "@/lib/i18n";
import { targetDate } from "@/data/release";
import createMiddleware from "next-intl/middleware";
import { routing } from "./i18n/routing";
import { getLanguage } from "@/lib/i18n";

export const config = {
  matcher: [
    "/",
    "/(es|en)",
    "/(es|en)/:path*",
    "/(es|en)/learn/:path*",
    "/(es|en)/admin/:path*",
    "/learn/:path*",
    "/admin/:path*",
    "/((?!api|_next/static|_next/image|favicon.ico|apple-touch-icon.png|favicon.svg|images/books|icons|manifest).*)",
  ],
};
// http://localhost:3000/learn/english/pronunciations
// export default createMiddleware(routing);
// export const middleware = createMiddleware(routing);

function isLocale(locale: string) {
  if (locale.length !== 2 && locale.length !== 5) {
    return false;
  }
  if (locale.length === 5 && locale[2] !== "-") {
    return false;
  }

  return true;
}

export async function middleware(request: NextRequest) {
  const [, locale] = request.nextUrl.pathname.split("/");

  if (!isLocale(locale)) {
    let nextLocale = request.cookies.get("NEXT_LOCALE")?.value;
    if (!nextLocale && request.headers.has("Accept-Language")) {
      const acceptLanguage = request.headers.get("Accept-Language") as string;
      nextLocale = getLanguage(acceptLanguage, routing.locales);
      // request.cookies.set("NEXT_LOCALE", nextLocale);

      if (nextLocale != routing.defaultLocale) {
        return NextResponse.redirect(
          new URL(`/${nextLocale}${request.nextUrl.pathname}`, request.url),
        );
      }
    }
    if (!nextLocale) {
      nextLocale = routing.defaultLocale;
      // request.cookies.set("NEXT_LOCALE", nextLocale);
    }
    request.nextUrl.pathname = `/${nextLocale}${request.nextUrl.pathname}`;
    request.headers.set("x-locale", nextLocale);
  } else {
    request.headers.set("x-locale", locale);
  }

  const showAdmin = false;

  const today = new Date();
  if (request.nextUrl.pathname.includes("/protected/") && false) {
    request.nextUrl.pathname = `/${locale}/protected/404`;
  } else if (today.getTime() < targetDate.getTime()) {
    request.nextUrl.pathname = `/${locale}/protected/countdown`;
  } else if (await showMaintenance()) {
    request.nextUrl.pathname = `/${locale}/protected/maintenance`;
  } else if (request.nextUrl.pathname.startsWith(`/${locale}/learn`)) {
    const user = await tokenInfo();
    if (!user) {
      return NextResponse.redirect(new URL(`/${locale}/login`, request.url));
    }

    request.headers.set("x-user", user.username);
  } else if (
    request.nextUrl.pathname.startsWith(`/${locale}/admin`) &&
    !showAdmin
  ) {
    request.nextUrl.pathname = `/${locale}/protected/404`;
  }

  request.nextUrl.pathname = request.nextUrl.pathname.replace("//", "/");

  const nonce = Buffer.from(crypto.randomUUID()).toString("base64");
  const cspHeader = `
      default-src 'self';
      script-src 'self' 'nonce-${nonce}' 'strict-dynamic';
      style-src 'self';
      img-src 'self' blob: data:;
      font-src 'self';
      object-src 'none';
      base-uri 'self';
      form-action 'self';
      frame-ancestors 'none';
      upgrade-insecure-requests;
    `;

  const contentSecurityPolicyHeaderValue = cspHeader
    .replace(/\s{2,}/g, " ")
    .trim();

  request.headers.set("x-nonce", nonce);
  request.headers.set(
    "Content-Security-Policy",
    contentSecurityPolicyHeaderValue,
  );

  request.headers.set("x-url", request.nextUrl.pathname);

  const handleI18nRouting = createMiddleware(routing);
  const response = handleI18nRouting(request);

  if (process.env.NODE_ENV === "production") {
    response.headers.set(
      "Content-Security-Policy",
      contentSecurityPolicyHeaderValue,
    );
  }

  return response;
}

requests.ts

import { getRequestConfig } from "next-intl/server";
import { routing } from "./routing";

type Locale = (typeof routing)["locales"][number];

export default getRequestConfig(async ({ requestLocale }) => {
  // Validate that the incoming `locale` parameter is valid
  let locale = await requestLocale;

  // Ensure that a valid locale is used
  if (!locale || !routing.locales.includes(locale as Locale)) {
    locale = routing.defaultLocale;
  }

  return {
    locale,
    messages: (await import(`../../messages/${locale}.json`)).default,
  };
});

routing.ts

import { defineRouting } from "next-intl/routing";
import { createNavigation } from "next-intl/navigation";

export const routingSettings = {
  // A list of all locales that are supported
  locales: ["en", "es"],

  // Used when no locale matches
  defaultLocale: "en",
  localePrefix: {
    mode: "as-needed",
    prefixes: {
      es: "/es",
    },
  },
};

export const routing = defineRouting({
  // A list of all locales that are supported
  locales: ["en", "es"],

  // Used when no locale matches
  defaultLocale: "en",
  // localePrefix: "never",
  localePrefix: {
    mode: "as-needed",
    prefixes: {
      es: "/es",
    },
  },
});

// Lightweight wrappers around Next.js' navigation APIs
// that will consider the routing configuration
export const { Link, redirect, permanentRedirect, usePathname, useRouter } =
  createNavigation(routing);

layout.tsx

import type { Metadata } from "next";
import localFont from "next/font/local";
import { ThemeProvider } from "@/components/theme-provider";
import { Toaster } from "@/components/ui/toaster";
import { headers } from "next/headers";

import "../globals.css";
import { NextIntlClientProvider } from "next-intl";
import { getMessages, setRequestLocale } from "next-intl/server";
import { NavbarComponent } from "@/components/navbar";
import { routing } from "@/i18n/routing";
import { notFound } from "next/navigation";
import { targetDate } from "@/data/release";

const geistSans = localFont({
  src: "../fonts/GeistVF.woff",
  variable: "--font-geist-sans",
  weight: "100 900",
});
const geistMono = localFont({
  src: "../fonts/GeistMonoVF.woff",
  variable: "--font-geist-mono",
  weight: "100 900",
});

export const metadata: Metadata = {
  title: "Create Next App",
  description: "Generated by create next app",
};

type LocaleProps = {
  children: React.ReactNode;
};

export async function LocaleLayout({ children }: LocaleProps) {
  const messages = await getMessages();

  return (
    <>
      <NextIntlClientProvider messages={messages}>
        {children}
      </NextIntlClientProvider>
    </>
  );
}

type RouterProps = {
  children: React.ReactNode;
  nonce: string | null;
};

function Theme({ children, nonce }: RouterProps) {
  "use client";

  return (
    <ThemeProvider
      attribute="class"
      defaultTheme="system"
      enableSystem
      disableTransitionOnChange
      nonce={nonce ?? undefined}
    >
      {children}

      <Toaster />
    </ThemeProvider>
  );
}

type Locale = (typeof routing)["locales"][number];

export default async function RootLayout({
  children,
  params: { locale },
}: Readonly<{
  children: React.ReactNode;
  params: { locale: string };
}>) {
  if (!routing.locales.includes(locale as Locale)) {
    notFound();
  }

  setRequestLocale(locale);

  const headersStore = headers();
  const nonce = headersStore.get("x-nonce");

  const today = new Date();

  if (today.getTime() < targetDate.getTime()) {
    return (
      <html lang={locale}>
        <body
          className={`${geistSans.variable} ${geistMono.variable} antialiased`}
        >
          <LocaleLayout>
            <Theme nonce={nonce}>
              <div className="overflow-y min-h-screen">
                <div>{children}</div>
              </div>
            </Theme>
          </LocaleLayout>
        </body>
      </html>
    );
  }

  return (
    <html lang={locale}>
      <body
        className={`${geistSans.variable} ${geistMono.variable} antialiased`}
      >
        <LocaleLayout>
          <Theme nonce={nonce}>
            <div className="overflow-y min-h-screen">
              <NavbarComponent />
              <div className="mt-20">{children}</div>
            </div>
          </Theme>
        </LocaleLayout>
      </body>
    </html>
  );
}
amannn commented 1 month ago

It seems like your issue is caused by your custom middleware code that writes to pathname before calling the middleware from next-intl.

I'll move this to a discussion since it's a usage question and not a bug report.