Closed iamstarkov closed 4 years ago
The problem with implementing a workaround in Error component is that it will throw notifying error in development which bothers me. Some improvement to my previous client side redirect:
What improved is that now it uses next/router in client side and the url replacement happens without a reload.
pages/_app.jsx
import App from 'next/app';
import Router from 'next/router';
export default class MyApp extends App {
render() {
const { Component, pageProps, router: { asPath, route } } = this.props;
// Next.js currently does not allow trailing slash in a route.
// This is a client side redirect in case trailing slash occurs.
if (pageProps.statusCode === 404 && asPath.length > 1 && asPath.endsWith('/')) {
const routeWithoutEndingSlash = route.replace(/\/*$/gim, '');
const asPathWithoutEndingSlash = asPath.replace(/\/*$/gim, '');
if (typeof window !== 'undefined') {
Router.replace(routeWithoutEndingSlash, asPathWithoutEndingSlash);
}
return null;
}
return <Component {...pageProps} />;
}
}
also thanks to @mbrowne for 404 fix :)
Took @cansin 's solution and added the ability to handle query parameters
MyError.getInitialProps = async ({ res, err, asPath }) => {
// Capture 404 of pages with traling slash and redirect them
const statusCode = res
? res.statusCode
: (err ? err.statusCode : 404);
if (statusCode && statusCode === 404) {
const [path, query = ''] = asPath.split('?');
if (path.match(/\/$/)) {
const withoutTrailingSlash = path.substr(0, path.length - 1);
if (res) {
res.writeHead(302, {
Location: `${withoutTrailingSlash}${query ? `?${query}` : ''}`,
});
res.end();
} else {
Router.push(`${withoutTrailingSlash}${query ? `?${query}` : ''}`);
}
}
}
@pinpointcoder can you provide examples of a url with trailing slash and query parameters happen at the same time? Are you thinking along the line of /blog/?123
?
Thanks everyone for some of your workarounds above. They worked!
However, do we have any official way to fix this issue from Next's team? This issue has been here for years.
Directory pages are not served with trailing slash in next export
@pinpointcoder can you provide examples of a url with trailing slash and query parameters happen at the same time? Are you thinking along the line of
/blog/?123
?
@coodoo Not him, but yes, unfortunately this happens a lot. I'm currently in the process of incrementally migrating a WordPress site onto Next.js, and for some reason, the original "developers" decided to force a trailing slash on every single URL, so we currently have tons of requests with both a trailing slash AND query parameters.
As we're about to migrate tons of blog posts for which the canonical URL currently includes a trailing slash, this is a giant pain in my ass right now.
I decided to implement a custom server to handle this and it turns out it's easy to do, and you can still use next.js's file-based routing system. That way you can rewrite the URL that next.js sees and the real URL still has a slash at the end:
const { createServer } = require('http')
const { parse } = require('url')
const next = require('next')
const conf = require('./next.config.js')
const PORT = process.env.PORT || 5000
const dev = process.env.NODE_ENV !== 'production'
const app = next({ dev, conf })
const handle = app.getRequestHandler()
app.prepare().then(() => {
createServer((req, res) => {
// If there is a slash at the end of the URL, remove it before sending it to the handle() function.
// This is a workaround for https://github.com/zeit/next.js/issues/5214
const url =
req.url !== '/' && req.url.endsWith('/')
? req.url.slice(0, -1)
: req.url
// Be sure to pass `true` as the second argument to `url.parse`.
// This tells it to parse the query portion of the URL.
const parsedUrl = parse(url, true)
handle(req, res, parsedUrl)
}).listen(PORT, err => {
if (err) throw err
console.log(`> Ready on http://localhost:${PORT}`)
})
})
@mbrowne We actually have a bunch of reasons to use a custom server, but the main thing that has prevented me from implementing one so far is the fact that you lose Automatic Static Optimization. Do you know if it's possible to manually specify static routes?
We don't need automatic static optimization for our app at the moment, so I haven't looked into it.
I am also using custom server but when you pass modified (without leading slash) url to handle
, SSR sees different url from client side.
I would prefer next
router to match url with leading slash without those nasty hacks.
2020 and this bug still hapens. Unbelievable
This is a bad bug that really needs to be fixed. /products
works, but /products/
doesn't. With this link
<Link href="/products">
<a>Products</a>
</Link>
I get
index.js:1 Warning: Prop `href` did not match. Server: "/products" Client: "/products/"
However, if I point the link to /products/
, visit the link, and refresh the page during development, I get a 404. This is quite a painful development experience.
This issue was first reported 1.5 years ago; can we please get an official fix? It's still present in 9.3.4.
I made redirection to non-trailing slash url instead of showing contents, for SEO reason.
app.prepare().then(() => {
createServer((req, res) => {
if (req.url !== '/' && req.url.endsWith('/')) {
res.writeHead(301, { Location: req.url.slice(0, -1) })
res.end()
}
handle(req, res, parse(req.url, true))
}).listen(PORT, err => {
if (err) throw err
console.log(`> Ready on http://localhost:${PORT}`)
})
})
For SEO, rel="canonical"
may help, but still need to fix this 404 issue.
This is a bad bug that really needs to be fixed.
/products
works, but/products/
doesn't. With this link<Link href="/products"> <a>Products</a> </Link>
I get
index.js:1 Warning: Prop `href` did not match. Server: "/products" Client: "/products/"
However, if I point the link to
/products/
, visit the link, and refresh the page during development, I get a 404. This is quite a painful development experience.This issue was first reported 1.5 years ago; can we please get an official fix? It's still present in 9.3.4.
I am also currently getting this issue.
Here's how I fixed it, https://medium.com/@thisisayush/handling-404-trailing-slash-error-in-nextjs-f8844545afe3
Here's how I fixed it, https://medium.com/@thisisayush/handling-404-trailing-slash-error-in-nextjs-f8844545afe3
Thank you, though this requires a custom server when developing locally, and one shouldn't be required.
@timneutkens Any chance a fix for this issue can be worked into the development schedule?
More importantly, the redirect solution doesn't work for those who are maintaining sites that area already set up to add a slash rather than remove one in production. I don't think the framework should be dictating this choice arbitrarily.
@AlexSapoznikov 's solution worked well for us with Netlify (which adds a trailing slash by default). Here is an advanced version that adds support for query params:
import App from "next/app";
export default class MyApp extends App {
render() {
const { Component, pageProps, router, router: { asPath } } = this.props;
// Next.js currently does not allow trailing slash in a route, but Netlify appends trailing slashes. This is a
// client side redirect in case trailing slash occurs. See https://github.com/zeit/next.js/issues/5214 for details
if (asPath && asPath.length > 1) {
const [path, query = ""] = asPath.split("?");
if (path.endsWith("/")) {
const asPathWithoutTrailingSlash = path.replace(/\/*$/gim, "") + (query ? `?${query}` : "");
if (typeof window !== "undefined") {
router.replace(asPathWithoutTrailingSlash, undefined, { shallow: true });
return null;
}
}
}
return <Component {...pageProps} />;
}
}
I apologize because I'm a Next JS newbie, although I have software development experience on other SDKs and platforms.
I think this "bug" surprised me the most. For me, it violated the "principle of least astonishment." I simply expected my /about/ and /about to work the same, since I placed an index.tsx into my /pages/about/ folder.
I first started making web sites in the late 1990s with HTML FTP'd to my server, and later moved on to PHP & Apache, and eventually Java servers. Now I specialize in mobile apps. It just feels weird to me that this behavior is not the default, and that I'd have to write a custom server page to fix it on my dev server.
I plan to do a static export, so it won't show up in production even if I don't write the custom server. It does make dev and debugging slightly more annoying though.
Can we get a "next dev" flag that fixes this so we lazy developers don't need to write extra routing logic just for dev/debug time?
Thanks!
p.s.: Yes, I do know that /about
and /about/
are completely different URLs. I just got really confused when I put an index.tsx
file inside my /pages/about/
folder, and discovered that it only works with the /about
path but does not work with /about/
. I would be less surprised if it was the other way around.
p.p.s.: It was extra confusing when I have a <Link></Link>
component that points to /about/
and it works as expected. Then when I hit refresh on my browser, it immediately 404s, even though the URL didn't change. That was very surprising. :-D
But wait, it gets worse! We added a custom checkForTrailingSlash
function inside _error.js
that would strip the trailing slash and redirect. This worked okay for a while until we (finally) added a custom 404 page and found that with a custom 404 page, Next.js completely bypasses Error
. This means none of your custom logic inside Error.getInitialProps
will work anymore - including a check for trailing slashes.
Guess I'll try the _app.js
solution others mentioned, as a custom server is just not a possibility quite yet.
@AlexSapoznikov 's solution worked well for us with Netlify (which adds a trailing slash by default). Here is an advanced version that adds support for query params:
import App from "next/app"; export default class MyApp extends App { render() { const { Component, pageProps, router, router: { asPath } } = this.props; // Next.js currently does not allow trailing slash in a route, but Netlify appends trailing slashes. This is a // client side redirect in case trailing slash occurs. See https://github.com/zeit/next.js/issues/5214 for details if (asPath && asPath.length > 1) { const [path, query = ""] = asPath.split("?"); if (path.endsWith("/")) { const asPathWithoutTrailingSlash = path.replace(/\/*$/gim, "") + (query ? `?${query}` : ""); if (typeof window !== "undefined") { router.replace(asPathWithoutTrailingSlash, undefined, { shallow: true }); return null; } } } return <Component {...pageProps} />; } }
There is a critical error in your code sample: requests to the index route with a query parameter will throw an error, since you end up attempting to pass just the query string to Next.js as the asPath
.
This fixes it:
if (asPath && asPath.length > 1) {
const [path, query = ''] = asPath.split('?');
if (path.endsWith('/') && path.length > 1) {
const asPathWithoutTrailingSlash =
path.replace(/\/*$/gim, '') + (query ? `?${query}` : '');
if (typeof window !== 'undefined') {
router.replace(asPathWithoutTrailingSlash, undefined, {
shallow: true,
});
return null;
}
}
}
To make this work with SSR I had to add the following to the @pjaws & @AlexSapoznikov solution:
static async getInitialProps({ Component, ctx, router }) {
/* Fixes the trailing-slash-404 bug for server-side rendering. */
const { asPath } = router;
if (asPath && asPath.length > 1) {
const [path, query = ""] = asPath.split("?");
if (path.endsWith("/") && path.length > 1) {
const asPathWithoutTrailingSlash =
path.replace(/\/*$/gim, "") + (query ? `?${query}` : "");
if (ctx.res) {
ctx.res.writeHead(301, {
Location: asPathWithoutTrailingSlash,
});
ctx.res.end();
}
}
}
return {
pageProps: Component.getInitialProps
? await Component.getInitialProps(ctx)
: {},
};
}
Probably it's a good idea to somehow generalize this functionality into a function that works both during SSR and during CSR and call it in both places (getInitialProps
and render
).
by
this will fix but the tittle wrong. Hmm
@AlexSapoznikov @pjaws
Your solution puts us in infinite loop:
if (asPath && asPath.length > 1) {
const [path, query = ''] = asPath.split('?');
if (path.endsWith('/') && path.length > 1) {
const asPathWithoutTrailingSlash =
path.replace(/\/*$/gim, '') + (query ? `?${query}` : '');
if (typeof window !== 'undefined') {
router.replace(asPathWithoutTrailingSlash, undefined, {
shallow: true,
});
return null;
}
}
}
Due to reasons beyond our control, we have to use the exportTrailingSlash
option in next.config.js
.
We want to have a link to another page but we want the link to be /somepage?param=whatever
.
It seems that next link converts this to /somepage/?param=whatever
and we get page not found.
Using the solution above solve the params problem, but then when going to a deployed page like /somepage/
it enters an infinite loop.
I think @ronyeh had made a really good point here, so I really want an official solution for this issue :(
To make this work with SSR I had to add the following to the @pjaws & @AlexSapoznikov solution:
static async getInitialProps({ Component, ctx, router }) { /* Fixes the trailing-slash-404 bug for server-side rendering. */ const { asPath } = router; if (asPath && asPath.length > 1) { const [path, query = ""] = asPath.split("?"); if (path.endsWith("/") && path.length > 1) { const asPathWithoutTrailingSlash = path.replace(/\/*$/gim, "") + (query ? `?${query}` : ""); if (ctx.res) { ctx.res.writeHead(301, { Location: asPathWithoutTrailingSlash, }); ctx.res.end(); } } } return { pageProps: Component.getInitialProps ? await Component.getInitialProps(ctx) : {}, }; }
Probably it's a good idea to somehow generalize this functionality into a function that works both during SSR and during CSR and call it in both places (
getInitialProps
andrender
).
This has worked for pages with getServerSideProps and now urls with trailing slashes are returning same page without 404. But there is one glitch, I have few pages that use dynamic routes and getStaticPaths, I can't use getServerSideProps onto them and thus when these dynamic routes are browsed with a trailing slash, they first return a 404 and then they redirect to the page.
I am working with an /api/test folder
it works for
and I just discovered that this doesn't work
not sure if this is related issue P/D exportTrailingSlash = true does not solve it
This is a very old issue, is there a reason it is not addressed for so long?
I'm not sure what is not working anymore.
My understating is that the requirements are as follows:
exportTrailingSlash: false | exportTrailingSlash: true | |
---|---|---|
url ends with / | Shouldn't work | Should work |
url does not end with / | Should work | Shouldn't work |
This works as expected where:
exportTrailingSlash: false
exportTrailingSlash: true
and an nginx converts url/
to url/index.html
From what I can see in @andrescabana86 This works where it shouldn't: GET /api/test/123/
whereas GET /api/test/
doesn't work and it shouldn't.
@Izhaki I tried both, deploying on prod... and for me is not working
and I am using exportTrailingSlash: true
I can try creating a public repo if you want, maybe I forgot something in the middle.
thank you for your answers
@andrescabana86 I'm not sure how much a public repo will help here - this may well be some configuration on the server you deploy onto.
We are testing our production builds (with exportTrailingSlash: true
) locally using this script in package.json
:
"serve:out": "docker run --rm -v $(pwd)/out:/static -p 5000:80 flashspys/nginx-static"
Please let me know if going in your browser to http://localhost:5000/api/test/
works.
(Note that $(pwd)
is on Mac/Linux - see this for windows)
@Izhaki the problem was about the fact that (as the initial report suggests) "trailing slash in link for legit page works for client side navigation but leads to not found bundle and 404 on hard refresh (ssr)". So there was a mismatch between the behavior of a client-side route change, versus a hard refresh. I am not sure if the problem persists with the latest version of Next.js. I can report back here once I test it.
Just tested with 9.4.1 and exportTrailingSlash: true
.
Going to http://localhost:6500/admin/
returns 404 when developing locally.
But the same path works when you export.
Note that exportTrailingSlash
hints this is for exports only.
What we do is use:
exportTrailingSlash: process.env.NODE_ENV === 'production'
That means things work as intended when we develop locally. And work properly when deployed (via export).
Isn't that the correct and viable solution for this?
If a URL does not work on development but does work on production, don't you think that is against the principle of least surprise? I think this should still be considered a bug.
^ That said, I am pretty sure previously on production there was a conflicting behavior between a page refresh vs a router.push event. I don't know if it is still the case.
@andrescabana86 @Izhaki exportTrailingSlash
is unrelated to this. That option relates to static exporting of Next.js applications. When true, example/index.html
is generated, whereas when it is false, example.html
is generated. My understanding is that exportTrailingSlash
has nothing to do with development mode.
I think one source of confusion is that when you have exportTrailingSlash
next.js adds a trailing slash to links. This happens in development as well I'm not sure it should do this? But anyhow, this is not only about example/index.html
vs example.html
- you need links to be modified as well.
If a URL does not work on development but does work on production, don't you think that is against the principle of least surprise? I think this should still be considered a bug.
I may be wrong, but exportTrailingSlash option was for nginx servers that are not configured to serve /something.html
when the url is /something
.
This is not the case with the next server used for local dev. So what works and what doesn't depends on what serves your app.
You can make a case that when exportTrailingSlash
is true, the next server should support routes ending with a trailing slash (although this will make the export
in exportTrailingSlash
somewhat irrelevant).
FWIW this is being worked on already #13333
I am not very experienced coder, using Next.js primarily for multi-paged landings. Apparently, I've been using the following workaround almost all of the time, unbeknownst to its effect. Here's stripped down version of it:
// In your server.js
server.get('/:id', (req, res) => {
const actualPage = `/${req.params.id}`
app.render(req, res, actualPage)
})
In my case the code is a little more complicated, because I am using it to support additional static url prefixes, etc. But this stripped down version seems to be working for the discussed issue just fine, regardless of the exportTrailingSlash
setting and its effect on Link
s. E.g. URLs /about
and /about/
work just fine.
In present form It essentially mimics the native routing of Next.js. The downside: it requires custom server.js
, and you will have to manually support it for "deeper" URLs (with additional "subfolders"), e.g. /company/about/
. But it seems to be relatively simple solution for those who already use custom server.js
in their project.
To make this work with SSR I had to add the following to the @pjaws & @AlexSapoznikov solution:
static async getInitialProps({ Component, ctx, router }) { /* Fixes the trailing-slash-404 bug for server-side rendering. */ const { asPath } = router; if (asPath && asPath.length > 1) { const [path, query = ""] = asPath.split("?"); if (path.endsWith("/") && path.length > 1) { const asPathWithoutTrailingSlash = path.replace(/\/*$/gim, "") + (query ? `?${query}` : ""); if (ctx.res) { ctx.res.writeHead(301, { Location: asPathWithoutTrailingSlash, }); ctx.res.end(); } } } return { pageProps: Component.getInitialProps ? await Component.getInitialProps(ctx) : {}, }; }
Probably it's a good idea to somehow generalize this functionality into a function that works both during SSR and during CSR and call it in both places (
getInitialProps
andrender
).This has worked for pages with getServerSideProps and now urls with trailing slashes are returning same page without 404. But there is one glitch, I have few pages that use dynamic routes and getStaticPaths, I can't use getServerSideProps onto them and thus when these dynamic routes are browsed with a trailing slash, they first return a 404 and then they redirect to the page.
@gauravkrp This is actually an extremely important addition, since @AlexSapoznikov solution will actually still return a 404 for the page to Google (since the redirect happens on the client). I imagine SEO is a major reason a lot of us are using Next.js in the first place.
I also think that putting this in getInitialProps
should just work all around, and the piece inside the main function is unnecessary at this point. The major caveat here is that you are losing Automatic Static Optimization by having this - probably better than a bunch of 404s, though.
For some sharing...
My project is Express
+ Next.js
.\
express 4.17.1
\
next 9.4.5-canary.7
Dynamic Runtime
// next.config.js
module.exports = {
exportTrailingSlash: false,
};
// app.js
const Next = require('next').default;
const NextApp = Next({ dev });
const NextHandler = NextApp.getRequestHandler();
NextApp.prepare();
app.get('*', (req, res) => NextHandler(req, res));
Static Export
Run next build
and next export -o dist/
// next.config.js
module.exports = {
exportTrailingSlash: true,
};
// app.js
app.use('/_next', express.static('dist/_next', { etag: true, index: false, maxAge: '365d', redirect: false, dotfiles: 'ignore' }));
app.use('/fonts', express.static('dist/fonts', { etag: true, index: false, maxAge: '365d', redirect: false, dotfiles: 'ignore' }));
app.use('/img', express.static('dist/img', { etag: true, index: false, maxAge: '365d', redirect: false, dotfiles: 'ignore' }));
app.use(express.static('./dist', { index: ['index.html'] }));
app.use((req, res) => {
res.Redirect('/404'); // <- Express will auto handle both /404 or /404/
});
I have no issue when redirect by clicking on client app,\
also hard refresh is working on static route
.
But it will 404 when hard refresh on dynamic route
,\
like /album/[id].jsx
or /album/123
,\
So I'm looking forward to fix this issue by using the following mechanism.
e.g.\
When hit 404 at /album/123
,\
server should continue to provide html content,\
browser will continue to load the page without issue,\
when Next.js boot up then next/router
should auto handle it.
is there any temporary solution to this issue on production?
We're about to land a feature fixing this—a day or so!
is there any temporary solution to this issue on production?
There are many in this thread, but I'm currently using what @gauravkrp posted recently, and it's working well for me.
You can keep track of the PR here: #13333
This has now been resolved in next@^9.4.5-canary.17
!
How long does it take for feature to get from canary to master?
This has now been resolved in
next@^9.4.5-canary.17
!
And how exactly it is resolved? just removing the trailing slash? if i access "www.site.com/help/" i get redirected to: "www.site.com/help" , can we have an option there we opt for leaving ending slash? accessing "www.site.com/help/" or "www.site.com/help" will leave or redirect or add "/" at the end to have: "www.site.com/help/"
@Valnexus see #13333, it includes an experimental option:
module.exports = {
experimental: {
trailingSlash: true
}
}
How long does it take for feature to get from canary to master?
When it's ready. There are still edge cases in the handling that are being solved. Once those have been fixed it can go to stable.
@timneutkens @Janpot
I tried the latest next canary (9.4.5-canary.27) but when I create test
page and I access www.example/test/
it redirects to www.example/test
I think behavior for both cases should be the same.
When access www.example/test/
it should stay on www.example/test/
.
When access www.example/test
it should stay on www.example/test
.
I test it on Nuxt.js, it works the same behavior that I describe above.
trailing slash in link for legit page works for client side navigation but leads to not found bundle and 404 on hard refresh (ssr)
Bug report
Describe the bug
let me know if title needs further clarification.
all relevant issues has been closed with reasoning that its been fixed in 6-canary (I believe it is not) or by improved serve (which is true only in perhaps production static export).
I'm rewriting my existing blog to next.js and i previously used trailing slashes. Latest
serve
can help with it once i build my next.js powered blog. But in order to fix dev env i need either to get rid of trailing slashes and utilize301 Moved Permanently
in prod; or live with broken trailing slash support in dev.To Reproduce
Here is minimal reproducible case (link to repro repo is below snippet):
Minimal reproducible repo https://github.com/iamstarkov/next.js-trailing-slash-bug-demo
git clone https://github.com/iamstarkov/next.js-trailing-slash-bug-demo
cd next.js-trailing-slash-bug-demo
yarn
yarn dev
http://localhost:3000/_next/static/development/pages/about.js
being 200edhttp://localhost:3000/_next/on-demand-entries-ping?page=/about/
being 200edhttp://localhost:3000/about/
being 404edhttp://localhost:3000/about/
Client pings, but there's no entry for page: /about/
Expected behavior
/about/
shouldnt be resolved as404 not found
/about/
should be resolved as200 ok
Client pings, but there's no entry for page: /about/
/about
and/about/
should work the same wayScreenshots
N/A
System information
Additional context
Add any other context about the problem here.
If you change this code in https://github.com/zeit/next.js/blob/459c1c13d054b37442126889077b7056269eeb35/server/on-demand-entry-handler.js#L242-L249
or
node_modules/next/dist/server/on-demand-entry-handler.js
locallyand restart
next dev
and open http://localhost:3000/ and click about link then:/about
/about/
:I think the problem (at least part of it) is in inability of onDemandEntryHandler's middleware to find page in entries if page has trailing slash.
I hope my 2 hours of investigation and preparation can help with fixing this issue.