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

URL resolution case sensitivity is inconsistent #21498

Open
nbouvrette opened this issue Jan 24, 2021 · 32 comments
Open

URL resolution case sensitivity is inconsistent #21498

nbouvrette opened this issue Jan 24, 2021 · 32 comments
Labels
bug Issue was opened via the bug report template.

Comments

@nbouvrette
Copy link
Contributor

nbouvrette commented Jan 24, 2021

What version of Next.js are you using?

10.0.5

What version of Node.js are you using?

14.15.0

What browser are you using?

Chrome

What operating system are you using?

Windows

How are you deploying your application?

Local

Describe the Bug

File system routes

http://localhost:3000/about -> renders pages/about.js
http://localhost:3000/ABOUT -> 404 not found

Result: case sensitive.

Redirects

Using this example: https://github.com/vercel/next.js/tree/canary/examples/redirects

http://localhost:3000/team -> redirects to /about -> renders pages/about.js
http://localhost:3000/TEAM -> redirects to /about -> renders pages/about.js

Result: case insensitive.

Rewrite

Using this example: https://github.com/vercel/next.js/tree/canary/examples/rewrites

http://localhost:3000/team -> rewrites to /about -> renders pages/about.js
http://localhost:3000/TEAM -> rewrites to /about -> renders pages/about.js

Result: case insensitive.

Expected Behavior

All URL resolution should have a consistent default behavior

Proposal:

  1. Resolving should be case insensitive, but, if the wrong case is used, a 301 redirect should send the request to the URL with the right case.
  2. Same behavior for UTF-8 characters (e.g. https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/toLocaleLowerCase to compare UTF-8 characters dependency on Pages with utf-8 name don't work properly under SSR #10084)

Benefits:

  • Can be good for people who "type" URLs with the wrong case (e.g. URLs shared using traditional media ads)
  • SEO friendly (no duplicate content found on the same case insensitive URL - Google indexes URL case-insensitively)

To Reproduce

See description - make sure to open each URL in a new incognito window to avoid Chrome cache hits.

@nbouvrette nbouvrette added the bug Issue was opened via the bug report template. label Jan 24, 2021
@eemeli
Copy link

eemeli commented Jan 29, 2021

I went down the rabbit hole a bit looking into this. I'm pretty sure that the reason for the case-insensitivity is this hard-coded option:

That was originally added in #9157 by @ijjk, specifically in commit 45942a9. I can't find any discussion though in the PR or the RFC #9081 about the case sensitivity of redirects & rewrites, so it seems like an oversight?

At least my gut feeling would be that the "right" thing to do here would be to keep redirects as case-insensitive, but make the rewrites case-sensitive. Would a PR for that be welcome?

@kwshi
Copy link

kwshi commented Feb 12, 2021

I am running into the same issue at work. We have an /about route; by default, since resolution is case-sensitive, /About sends us to a 404, and we want that instead to redirect to /about. But redirects are case-insensitive, so setting

{
  source: '/About',
  destination: '/about',
  permanent: true,
}

sends us into an infinite redirect loop, because /about also matches the redirect rule for /About.

Per these discussions, the only available "solution" to this problem right now is to implement redirecting with a dynamic route: (taken from the SO answer)

// pages/[route].js

import { useEffect } from 'react'
import { useRouter } from 'next/router'
import Error from 'next/error'

export default function ResolveRoute() {
    const router = useRouter()

    useEffect(() => {
        if (router.pathname === router.pathname.toLowerCase()) {
            // Route is already lowercase but matched catch all
            // page not found, display 404
            return <Error statusCode={404} />
        } else {
            router.push(router.pathname.toLowerCase())
        }
    })

    return <p>Redirecting...</p>
}

But this solution seems quite hacky/suboptimal to me. The real problem, I think, is the disparity between resolution & redirect case-sensitivity. Either both should be case-insensitive, or both sensitive.

Personally, I enjoy @nbouvrette's suggestion of making resolution case-insensitive & auto-redirecting to the canonically-cased path, but technically that's a compatibility-breaking change--it's possible, though probably very rare (and arguably bad practice anyway) that some sites currently rely on case-insensitive routing to send, e.g., /A to a different page than /a. An alternative, less breaking fix, which I think would be still preferable to the current behavior, is to make matching of redirect rules case-sensitive.

@nbouvrette
Copy link
Contributor Author

nbouvrette commented Feb 13, 2021

Personally, I enjoy @nbouvrette's suggestion of making resolution case-insensitive & auto-redirecting to the canonically-cased path, but technically that's a compatibility-breaking change--it's possible, though probably very rare (and arguably bad practice anyway) that some sites currently rely on case-insensitive routing to send, e.g., /A to a different page than /a. An alternative, less breaking fix, which I think would be still preferable to the current behavior, is to make matching of redirect rules case-sensitive.

Another idea, if we want to avoid the breaking change could be to add a new caseSensitive option to both redirects and rewrites and by default, it would be set to false.

It would be great to get some feedback from Vercel on this issue.

@tnovau
Copy link

tnovau commented May 25, 2021

I'm having the same issue, is there any feedback about this?

Solved it using rewrites

{
        source: '/(a|A)(b|B)(o|O)(u|U)(t|T)',
        destination: '/about',
}

@ptudan
Copy link

ptudan commented Jul 22, 2021

I'm having the same issue, is there any feedback about this?

Solved it using rewrites

{
        source: '/(a|A)(b|B)(o|O)(u|U)(t|T)',
        destination: '/about',
}

Thanks! This is by far the best to do it right now imo. I did notice one infinite redirect with this method in my dev testing, but I'm hoping that was a red herring. Agree with above that a setting on capitalization sensitivity would be helpful, but at least for now we can use the rewrite solution very easily.

@psoaresbj
Copy link

psoaresbj commented Aug 18, 2021

I am facing the same issue but with a language path 🤦

We've launched an initial version of a project with pt-br so now there's some public shared urls with the prefix. Then we've changed to pt-BR to follow standards when adding other languages.

Tried both strategies: redirects and rewrites. Both are not working...

Not even getting our default 404 page but the vercel one:
Screenshot 2021-08-18 at 08 13 00

@joaonew
Copy link

joaonew commented Aug 26, 2021

A functionality for automatically lowering URLs would be great, specially for SEO since Google understand that a URL /about is different from /About.

@knightspore
Copy link

Same issue as @psoaresbj - super frustrating!

@matheusrgarcia
Copy link

I'm having the same issue, is there any feedback about this?

Solved it using rewrites

{
        source: '/(a|A)(b|B)(o|O)(u|U)(t|T)',
        destination: '/about',
}

Works nice for me, thanks a lot

@cody-ta
Copy link

cody-ta commented Nov 23, 2021

I'm having the same issue, is there any feedback about this?

Solved it using rewrites

{
        source: '/(a|A)(b|B)(o|O)(u|U)(t|T)',
        destination: '/about',
}

Hello guys, how can I do this with :slug or :path?

@AnnaVih
Copy link

AnnaVih commented Nov 25, 2021

same problem! How to deal with :slug if I need it uppercase?

@riffbyte
Copy link

riffbyte commented Dec 6, 2021

Same problem here!
Unless we duplicate all of our locale codes to also have lowercase equivalents, we can only either get en-US or en-us type codes to work, but not both. This is not a good option, since we already have 100 locales, which is max recommended by Next.
If this is really about a single config parameter in the Next code, it would be so nice to remedy that 🙏🏻

@nbouvrette
Copy link
Contributor Author

Unless we duplicate all of our locale codes to also have lowercase equivalents, we can only either get en-US or en-us type codes to work, but not both.

FYI this goes a bit beyond this issue, but we have resolved most of the current challenges with this solution: https://github.com/Avansai/next-multilingual

@pedrorosaes
Copy link

pedrorosaes commented Dec 7, 2021

Next.js 12.0 brings a new feature wich can help with this problem. We can use Middleware to add custom routing rules:

For example, here's a next.config.js file with support for a few languages. Note the "default" locale has been added intentionally.

// next.config.js
module.exports = {
  i18n: {
    locales: ['default', 'en', 'de', 'fr'],
    defaultLocale: 'default',
    localeDetection: false,
  },
  trailingSlash: true,
}
// pages/_middleware.ts

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

const PUBLIC_FILE = /\.(.*)$/

export function middleware(request: NextRequest) {
  const shouldHandleLocale =
    !PUBLIC_FILE.test(request.nextUrl.pathname) &&
    !request.nextUrl.pathname.includes('/api/') &&
    request.nextUrl.locale === 'default'

  return shouldHandleLocale
    ? NextResponse.redirect(`/en${request.nextUrl.href}`)
    : undefined
}

This Middleware skips adding the default prefix to API Routes and public files like fonts or images. If a request is made to the default locale, we redirect to our prefix /en.

@illiakovalenko
Copy link

Any updates on the issue? Unfortunately still not fixed

@vc-ca
Copy link

vc-ca commented Jul 8, 2022

In the pages/_app.js file, I added:

function checkUppercase(str) {
    for (var i = 0; i < str.length; i++) {
      if (
        str.charAt(i) == str.charAt(i).toUpperCase() &&
        str.charAt(i).match(/[a-z]/i)
      ) {
        return true;
      }
    }
    return false;
  }

  useEffect(() => {
    if (checkUppercase(router.pathname)) {
      router.push(`/${router.pathname.toLowerCase()}`); 
    }
  }, []);

There's probably a more eloquent way to handle this, but this will work for any capitalized url entered.

@tekstrand
Copy link

This would be great to get a path forward on from Vercel

@SiimTulev
Copy link

In the pages/_app.js file, I added:

function checkUppercase(str) {
    for (var i = 0; i < str.length; i++) {
      if (
        str.charAt(i) == str.charAt(i).toUpperCase() &&
        str.charAt(i).match(/[a-z]/i)
      ) {
        return true;
      }
    }
    return false;
  }

  useEffect(() => {
    if (checkUppercase(router.pathname)) {
      router.push(`/${router.pathname.toLowerCase()}`); 
    }
  }, []);

There's probably a more eloquent way to handle this, but this will work for any capitalized url entered.

I wonder if it slows down the website a little?

@babkenmes
Copy link

In the pages/_app.js file, I added:

function checkUppercase(str) {
    for (var i = 0; i < str.length; i++) {
      if (
        str.charAt(i) == str.charAt(i).toUpperCase() &&
        str.charAt(i).match(/[a-z]/i)
      ) {
        return true;
      }
    }
    return false;
  }

  useEffect(() => {
    if (checkUppercase(router.pathname)) {
      router.push(`/${router.pathname.toLowerCase()}`); 
    }
  }, []);

There's probably a more eloquent way to handle this, but this will work for any capitalized url entered.

I wonder if it slows down the website a little?

Besides that, it is not a redirect so if you are concerned about SEO better add a specific redirect to next.config.js or add middleware

import { NextResponse } from "next/server";

const Middleware = (req) {
  if (req.nextUrl.pathname === req.nextUrl.pathname.toLowerCase())
    return NextResponse.next();

  return NextResponse.redirect(
    `${req.nextUrl.origin + req.nextUrl.pathname.toLowerCase()}`
  );
};

export default Middleware;

@will-hancock
Copy link

Agree...

Just had the issue where the Author changed a slug;
/About ==> /about

/About resulted in a 404, it didn't resolve to the /about page.

Possible Solution: OK, we can add a redirect for /About ==> 308 /about
Nope; this resulted in a browser error: "ERR_TOO_MANY_REDIRECTS"

@magicspon
Copy link

Any news on this... seems like a pretty major issue. I've got a bunch of urls with mixed casing.

@brysonjonesy
Copy link

It would be great if there was something we could put in next.config.js to dynamically redirect urls to lowercase without having to do the middleware hack.

@israteneda
Copy link

I was able to solve the issue of locales with rewrites, with this code:

// next.config.js
async rewrites() {
  return [
    {
      source: '/([eE][sS]-[eE][cC])/:path*',
      destination: '/es-EC/:path*',
      locale: false,
    },
  ];
}

@sandyvb
Copy link

sandyvb commented Mar 19, 2023

I adjusted this for nextjs slug pages (i.e. [id].js)
Use router.asPath which returns "/some-path"

const checkUppercase = (str) => {
    return /[A-Z]/.test(str)
  }

  useEffect(() => {
    if (checkUppercase(router.asPath)) {
      router.push(`${router.asPath.toLowerCase()}`)
    }
  }, [])

@christianjuth
Copy link

christianjuth commented Apr 6, 2023

Same issue here, but I noticed something I didn't see mentioned yet. In the route tree, only leaf nodes are case sensitive. For example, if your url is /one/two the one is case insensitive but the two is case sensitive. So /ONE/two works, but /one/TWO does not.

I reached out to Vercel support initially because I thought this was a build problem with my deployment. They suggested adding some middleware to normalize urls.

// src/middleware.ts
import { rewrite } from "@vercel/edge"

export function middleware(request: Request) {
  const url = new URL(request.url)

  // Don't modify files
  // Without this bundled js files and assets (e.g. fonts, images) will break
  if (/\.[a-z0-9]+$/i.test(url.pathname)) {
    return
  }

  // Note: we're excluding origin, search, and hash from normalization
  const lowerCaseUrl = new URL(
    `${url.origin}${url.pathname.toLowerCase()}${url.search}${url.hash}`
  )

  if (lowerCaseUrl.toString() !== url.toString()) {
    // For some reason Vercel rewrite doesn't seem to be working
    // rewrite(lowerCaseUrl)

   return Response.redirect(lowerCaseUrl.toString())
  }
}

However, this solution has some problems. For example, this forces any dynamic segment of my url to be lowercased. I would rather a url like /blog/[slug] have blog be case insensitive, but let the page (which fetches the article) decide how to handle the slug.

I have not tested this using the app directory.

@clearly-outsane
Copy link

This is like 2 years old now, is there no better way to do it?

kodiakhq bot pushed a commit that referenced this issue Jun 7, 2023
This adds an experimental `caseSensitiveRoutes` config that currently applies for `rewrites`, `redirects`, and `headers` to change the default of case-insensitive. 

x-ref: [slack thread](https://vercel.slack.com/archives/C02K2HCH5V4/p1686080359514479?thread_ts=1686077053.623389&cid=C02K2HCH5V4)
x-ref: [slack thread](https://vercel.slack.com/archives/C057RG6Q9MX/p1686078875948069?thread_ts=1686077882.133609&cid=C057RG6Q9MX)
x-ref: #21498
@muhammadali-pro
Copy link

I am still facing same issue. i have a page in App directory i.e App/About/page.tsx domain.com/About works but domain.com/about results in 404. How to fix it?

hydRAnger pushed a commit to hydRAnger/next.js that referenced this issue Jun 12, 2023
This adds an experimental `caseSensitiveRoutes` config that currently applies for `rewrites`, `redirects`, and `headers` to change the default of case-insensitive. 

x-ref: [slack thread](https://vercel.slack.com/archives/C02K2HCH5V4/p1686080359514479?thread_ts=1686077053.623389&cid=C02K2HCH5V4)
x-ref: [slack thread](https://vercel.slack.com/archives/C057RG6Q9MX/p1686078875948069?thread_ts=1686077882.133609&cid=C057RG6Q9MX)
x-ref: vercel#21498
@AndreiIlisei
Copy link

I am facing a similar issue as well, and the answers from above did not work in my case. I see some new commits have been pushed towards this case, so any idea when the experimental caseSensitiveRoutes will be available?

I have my issue described here, if anyone came across it as well https://stackoverflow.com/questions/76605645/nextjs-uppercase-locales-redirect-to-lowercase .

Thank you!

@timneutkens
Copy link
Member

You can try using it using the experimental version:

// next.config.js
// @ts-check
/**
 * @type {import('next').NextConfig}
 **/
const nextConfig = {
  /* config options here */
  experimental: {
    caseSensitiveRoutes: true
  }
}
 
module.exports = nextConfig

@AndreiIlisei
Copy link

It does not work unfortunately, but thank you!

@SharmaTushar1
Copy link

SharmaTushar1 commented Aug 14, 2023

This is such a major issue. I was running the app locally on my machine and it worked fine. I'm using Windows so it worked fine. Windows deals with it internally. I later asked about this online and was told that Vercel deployment uses Linux which is case-sensitive. So when I was trying to go to About it was not working sadly.

Also, what about cases like AbouT, ABOUT, AbOUT, and all other cases? It should be case insensitive to handle all these cases too. Am I right?

thomasdax98 pushed a commit to vivid-planet/comet-starter that referenced this issue Mar 19, 2024
Creates rewrites for redirect loop, as case sensitivity in Next.js is
inconsistent (see also
vercel/next.js#21498 (comment)).

Background:

Sometimes you want a redirect from an uppercase to a lowercase URL (e.g.
/Example to /example) for SEO purposes. This can be created in our admin
without problems. The casing is saved correctly in the DB and
transferred to Next.

The problem is that this creates a redirect loop in the site. The reason
is that the case sensitivity of Next.js is inconsistent. The delivery of
pages is case sensitive. The redirects are case insensitive:

Without redirect:

http://localhost:3000/example -> page is delivered
http://localhost:3000/Example -> 404

With redirect:

http://localhost:3000/example -> Redirect to /example -> Loop
http://localhost:3000/Example -> Redirect to /example -> Loop

Also described here: vercel/next.js#21498

---------

Co-authored-by: Johannes Obermair <48853629+johnnyomair@users.noreply.github.com>
thomasdax98 added a commit to vivid-planet/comet that referenced this issue Mar 21, 2024
Creates rewrites for redirect loop, as case sensitivity in Next.js is
inconsistent (see also
vercel/next.js#21498 (comment)).

--- 

Background:

Sometimes you want a redirect from an uppercase to a lowercase URL (e.g.
`/Example` to `/example`) for SEO purposes. This can be created in our
admin without problems. The casing is saved correctly in the DB and
transferred to Next.

The problem is that this creates a redirect loop in the site. The reason
is that the case sensitivity of Next.js is inconsistent. The delivery of
pages is case sensitive. The redirects are case insensitive:

Without redirect:

http://localhost:3000/example -> page is delivered
http://localhost:3000/Example -> 404

With redirect:

http://localhost:3000/example -> Redirect to /example -> Loop
http://localhost:3000/Example -> Redirect to /example -> Loop

Also described here: vercel/next.js#21498

---------

Co-authored-by: Thomas Dax <thomas.dax@vivid-planet.com>
jamesricky pushed a commit to vivid-planet/comet that referenced this issue Mar 25, 2024
Creates rewrites for redirect loop, as case sensitivity in Next.js is
inconsistent (see also
vercel/next.js#21498 (comment)).

---

Background:

Sometimes you want a redirect from an uppercase to a lowercase URL (e.g.
`/Example` to `/example`) for SEO purposes. This can be created in our
admin without problems. The casing is saved correctly in the DB and
transferred to Next.

The problem is that this creates a redirect loop in the site. The reason
is that the case sensitivity of Next.js is inconsistent. The delivery of
pages is case sensitive. The redirects are case insensitive:

Without redirect:

http://localhost:3000/example -> page is delivered
http://localhost:3000/Example -> 404

With redirect:

http://localhost:3000/example -> Redirect to /example -> Loop
http://localhost:3000/Example -> Redirect to /example -> Loop

Also described here: vercel/next.js#21498

---------

Co-authored-by: Thomas Dax <thomas.dax@vivid-planet.com>
@alesbencina
Copy link

alesbencina commented Mar 26, 2024

I've resolved the issue by adding a middleware function which is the first one in the middleware chain.
!important!
Skip the routes that have tokens in the url!

export const handleUppercase = (
  request: NextRequest,
): NextResponse | boolean => {
  const shouldHandleUppercase =
    !PUBLIC_FILE.test(request.nextUrl.pathname) &&
    !request.nextUrl.pathname.includes('/api/') &&
    !request.nextUrl.pathname.includes('/_next');

  // We have to be careful here to handle only routes and not _next, api or files.
  if (shouldHandleUppercase) {
    // Convert pathname to lowercase
    const lowercasePathname = request.nextUrl.pathname.toLowerCase();

    // Check if the requested pathname is already lowercase
    if (request.nextUrl.pathname === lowercasePathname) {
      // If it's already lowercase, proceed with the request
      return false;
    } else {
      // If it's not lowercase, redirect to the lowercase version
      return NextResponse.redirect(
        `${request.nextUrl.origin + lowercasePathname}`,
      );
    }
  }
  return false;
};

export function middleware(request: NextRequest) {
    // Handles the uppercases and creates a redirect to lower case route.
    const handleUppercaseRedirect = handleUppercase(request);
    if (handleUppercaseRedirect) return handleUppercaseRedirect;
  }

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Issue was opened via the bug report template.
Projects
None yet
Development

No branches or pull requests