Next.js rewrite produces a 301 redirect on certain URLs

We have a Next.js site running on Netlify. The production URL is https://www.metrie.com/, however this issue affects all our environments (dev, staging, and production) as well as deploy previews.

We recently upgraded Next.js to 12.3, which required us to rewrite our middleware from the older beta standard to the final stable version.

Since that upgrade, I’ve been seeing odd behavior on just a few specific URLs, for example: Aperçu des produits | Metrie

The site currently supports 3 locales (EN, FR, ES) and uses Middleware to localize the routes for French and Spanish. The actual defined Next.js route is /products, but we use middleware to rewrite (not redirect) /fr/produits and /es/productos to that route.

The relevant portion of the Middleware file looks like this:

if (matchedRoute) {
	const source = matchedRoute.source.replace(':slug*', '*');
	const p = new URLPattern({ pathname: source });
	const routePattern = p.exec(href);

	let redirectUrl = `${protocol}//${host}/${locale}${matchedRoute.destination}`;
        let slug = '';
	if (routePattern?.pathname.groups?.[0]) {
		slug = routePattern.pathname.groups?.[0];
	}

        redirectUrl = redirectUrl.replace(':slug*', slug);
	return NextResponse.rewrite(new URL(redirectUrl));
}

This works, and Aperçu des produits | Metrie is fully accessible on the site, both by directly entering the URL and through the Next.js client-side routing. However, if you type the URL in directly for just this URL and a couple of its child routes (for example, Solutions de produits | Metrie), Netlify is serving a 301 redirect to the English route.

Here’s an example of the response to GET /fr/produits:

age: 40441
cache-control: public, max-age=0, must-revalidate
content-encoding: gzip
content-type: text/html; charset=UTF-8
date: Mon, 02 Jan 2023 05:32:03 GMT
etag: W/"4167ea6dfb5bacbb4a8b99d0578f558c-ssl-df"
location: /fr/products
server: Netlify
strict-transport-security: max-age=31536000
vary: Accept-Encoding
x-middleware-rewrite: /fr/products/
x-nf-request-id: 01GNSP0E8MYH7KQTSP9VGJ86H7

Which is then followed immediately by a GET for /fr/products (response follows):

age: 40440
cache-control: public, max-age=0, must-revalidate
content-encoding: gzip
content-type: text/html; charset=UTF-8
date: Mon, 02 Jan 2023 05:32:04 GMT
etag: W/"4167ea6dfb5bacbb4a8b99d0578f558c-ssl-df"
server: Netlify
strict-transport-security: max-age=31536000
vary: Accept-Encoding
x-middleware-next: 1
x-nf-request-id: 01GNSP0EAX2Y6DEVP5SNZE6W6H

The content is served correctly at this URL, but it’s producing a bad-SEO situation where identical content is served from two different URLs.

According to the Next.js documentation, the Middleware rewrite() method should transparently proxy the URL and should not produce a 301 redirect or change the URL, so I’m having trouble figuring out where this is coming from.

I’ve already checked the _redirects file (which is quite large on this site) as well as the generated netlify.toml file and verified that there is no server-side redirect from /fr/produits -> /fr/products. I’ve also tested it with and without trailing slashes and the behavior is identical.

Finally, it’s interesting to note that other URLs that are rewritten by middleware don’t display the same issue. For example, /fr/où-acheter never visibly redirects to /fr/where-to-buy, even though that is the actual Next.js route.

Thanks in advance for any help!

Hey @daverodriguez,

Thank you for a detailed post. From some investigation, there doesn’t seem to be any obvious reasons for this redirect. This has thus been escalated to the devs and we are now waiting for their feedback.

We will let you know as soon as we have more information.

Netlify team, any updates here? If not yet, can you let us know where we are in the queue?

Hey @daverodriguez,

Our devs have been trying to reproduce the issue, but haven’t been able to do that yet, which is why this is taking so long. Do you think you can share your repo or a minimal reproduction repo?

Hrishikesh, yes, we’re able to share the repository. Would you like to DM me and let me know what Github username I should share with? Thanks!

Hi @daverodriguez :wave:t6: , sorry for the delay in response. I opened a ticket in our help desk so you can share this directly with our support engineers stay tuned to your email.

We ended up figuring out the issue on our own, I’m going to close this ticket.

Awesome thank you for letting us know!