46

I am using Node.js and Express and I have the following routing :

app.get('/', function(req,res){
    locals.date = new Date().toLocaleDateString();

    res.render('home.ejs', locals);
});

function lessonsRouter (req, res, next)
{
    var lesson = req.params.lesson;
    res.render('lessons/' + lesson + '.ejs', locals_lessons);
}

app.get('/lessons/:lesson*', lessonsRouter);


function viewsRouter (req, res, next)
{
    var controllerName = req.params.controllerName;
    res.render(controllerName + '.ejs', locals_lessons);
}
app.get('/:controllerName', viewsRouter);

I have a Disqus widget on my lessons pages and I have noticed a strange behavior that when going to myapp.com/lessons and myapp.com/lessons/ I get two different pages (on of them had a comment I previously added in Disqus and the other one doesn't have a comment).

Is there a way to "canonize" all of my urls to be without trailing slashes ? I have tried to add the strict routing flag to express but the results were the same

Thanks

pravdomil
  • 2,961
  • 1
  • 24
  • 38
Michael
  • 22,196
  • 33
  • 132
  • 187
  • 1
    What version of express are you using? In 3.x, the default behavior (without turning `strict routing` on) makes `/foo` and `/foo/` appear the same to the router. Given that the page is rendering either way, my first guess is that this is a browser caching issue, but without knowing anything more about disqus, I can't be sure. – David Weldon Nov 18 '12 at 17:29
  • @DavidWeldon express 3. This might be `Disqus` seeing it as two different addresses. In any case, how should I redirect any address with trailing `/` to an address without ? This way, even if the user enters `/` in the browser it will get redirect to the right path – Michael Nov 18 '12 at 17:55
  • If you know the issue will always be isolated to one specific route, I'd add the redirect to that specific route handler. If not, I'd go with a middleware solution like Tolga gave below. – David Weldon Nov 18 '12 at 19:06
  • 2
    "strict routing: Enable strict routing, by default "/foo" and "/foo/" are treated the same by the router" `app.set('strict routing', true);` ~ src: http://expressjs.com/api.html#app-settings – Bas van Ommen Sep 16 '13 at 14:49

8 Answers8

104

The answer by Tolga Akyüz is inspiring but doesn't work if there is any characters after the slash. For example http://example.com/api/?q=a is redirected to http://example.com/api instead of http://example.com/api?q=a.

Here is an improved version of the proposed middleware that fixes the problem by adding the original query to the end of the redirect destination URL. The version also has a few safety features described in the update notes.

app.use((req, res, next) => {
  if (req.path.slice(-1) === '/' && req.path.length > 1) {
    const query = req.url.slice(req.path.length)
    const safepath = req.path.slice(0, -1).replace(/\/+/g, '/')
    res.redirect(301, safepath + query)
  } else {
    next()
  }
})

Update 2016: As noted by jamesk and stated in RFC 1738, the trailing slash can only be omitted when there is nothing after the domain. Therefore, http://example.com?q=a is an invalid URL where http://example.com/?q=a is a valid one. In such case, no redirection should be done. Fortunately, the expression req.path.length > 1 takes care of that. For example, given the URL http://example.com/?q=a, the path req.path equals to / and thus the redirection is avoided.

Update 2021: As discovered by Matt, a double slash // in the beginning of the path causes a dangerous redirection. For example the URL http://example.com//evil.example/ creates a redirect to //evil.example that is interpreted as http://evil.example or https://evil.example by the victim's browser. Also, noted by Van Quyet, there can be multiple trailing slashes that should be handled gracefully. Due to these findings, I added a line that safeguards the path by replacing all consequent slashes by a single /. I trust the performance overhead caused by the safeguard to be negligible because the regexp literals are only compiled once. Additionally, the code syntax was updated to ES6.

Tjorben
  • 3
  • 3
Akseli Palén
  • 27,244
  • 10
  • 65
  • 75
  • 1
    I wonder how many people upvote Tolga's answer, run it for a few days, and then come back here when they realize this is missing. – Patrick Lee Scott Apr 27 '16 at 18:00
  • 1
    Note this practice diverges from the format prescribed by RFC 1738: http://stackoverflow.com/a/1617074/5352503 – jamesk Sep 04 '16 at 21:51
  • @jamesk I updated the answer regarding the issue you pointed out. Fortunately, the middleware I proposed already handles it (see the note I added). I guess the confusion was caused by my poor url examples. The urls are now replaced with correct ones, thanks to you. – Akseli Palén Sep 05 '16 at 17:21
  • 1
    the format prescribed by RFC1738 have been overruled by other RFC (2396 and 3986) and by the URL Standard. it is valid for an url to be of the form `http://example.com?q=a`. See https://stackoverflow.com/a/42193734/6205646 – Félix Brunet Feb 26 '19 at 19:53
  • 1
    If we have URL: `http://example.com/abc/xyz/////` (more than one slash), we should not redirect it. I suggest to edit the condition to: `(req.path.substr(-1) === '/' && req.path.substr(-2, 1) !== '/' && req.path.length > 1)` – Văn Quyết Apr 08 '19 at 08:23
  • 1
    This code is dangerous. It potentially creates an open redirect. For example if we make the request `http://example.com//evil.com/` (note the double slash at start of path and trailing slash) this will redirect to `//evil.com` which is interpreted as a protocol-relative link to evil.com – Matt Sep 27 '21 at 06:33
  • @Matt Thanks for discovering this hazard! I reviewed the answer and added a line with notes that prevents such malicious redirection. Let me know if you find any problems with this approach. – Akseli Palén Sep 27 '21 at 18:45
  • @AkseliPalén thanks for the edit! Is a pretty subtle bug. While investigating this I found a few major sites which were vulnerable to this issue, I already disclosed it to them :) – Matt Oct 06 '21 at 03:56
58

Try adding a middleware for that;

app.use((req, res, next) => {
  const test = /\?[^]*\//.test(req.url);
  if (req.url.substr(-1) === '/' && req.url.length > 1 && !test)
    res.redirect(301, req.url.slice(0, -1));
  else
    next();
});
dude
  • 5,678
  • 11
  • 54
  • 81
tolgaio
  • 3,206
  • 1
  • 19
  • 18
  • 6
    This works. I prefer `req.url.slice(0, -1)` to `req.url.substring(0, req.url.length-1)`, but that's just a matter of style. – David Weldon Nov 18 '12 at 19:08
  • I'm still learning express middleware, stuff are becoming more clear, where should i put this code ? I tried to put it straight after `app.configure` definition but it didn't catch – Michael Nov 18 '12 at 19:48
  • You can put that in anywhere before the `app.use(app.router)`, if you send the whole app configuration i can tell you more specifically, cheers. – tolgaio Nov 18 '12 at 19:52
  • 2
    @TolgaAkyüz I suspect you meant `res.redirect` instead of `req.redirect` ? – Michael Nov 18 '12 at 21:34
  • Also, I am getting a redirect loop when accessing root url `myapp.com` so maybe you can change the condition to `if(req.url.substr(-1) == '/' && req.url.length > 1)` – Michael Nov 18 '12 at 22:16
  • Yes, you are right, i was in a hurry, updated the answer, cheers. – tolgaio Nov 18 '12 at 22:42
  • 8
    Note that with this middleware the redirection doesn't work if there is anything after the slash. For example `http://example.com/lessons/?` won't be redirected even though `http://example.com/lessons/` will. – Akseli Palén Apr 02 '13 at 19:55
  • req.path is the expressjs property. req.url is inherited from node's http module. can also use `req.path[req.path.length-1] === '/'` – darethas May 05 '16 at 16:12
  • This only works for GET requests. In case you wish to redirect POST requests check for req.method and do an ajax request manually. – flaudre Jul 12 '16 at 09:59
  • I would add method as well in case of a POST so that the POST data is carried over to the new URL: `let method = (req.method === 'POST') ? 307 : 301; res.redirect(method, url);` – Vedran Sep 03 '17 at 09:58
  • This won't work in a Next.js environment with requests like e.g. `http://localhost:3000/_next/on-demand-entries-ping?page=/`. This example completely misses any query param possibilities – dude Feb 07 '18 at 14:59
  • Fails if there are query params – Ali Azhar Aug 15 '18 at 12:14
27

The connect-slashes middleware was designed specifically for this need: https://npmjs.org/package/connect-slashes

Install it with:

$ npm install connect-slashes

Read the full documentation: https://github.com/avinoamr/connect-slashes

Martti Laine
  • 12,655
  • 22
  • 68
  • 102
Roi Avinoam
  • 271
  • 3
  • 2
10

I'm adding this answer because I had too many issues with other solutions.

/**
 * @param {express.Request} req
 * @param {express.Response} res
 * @param {express.NextFunction} next
 * @return {void}
 */
function checkTrailingSlash(req, res, next) {
  const trailingSlashUrl = req.baseUrl + req.url;
  if (req.originalUrl !== trailingSlashUrl) {
    res.redirect(301, trailingSlashUrl);
  } else {
    next();
  }
}

router.use(checkTrailingSlash);

This will translate:

/page ==> /page/
/page?query=value ==> /page/?query=value
ShortFuse
  • 5,970
  • 3
  • 36
  • 36
7

One liner:

router.get('\\S+\/$', function (req, res) {
  return res.redirect(301, req.path.slice(0, -1) + req.url.slice(req.path.length));
});

This will only catch the url's that need to be redirected, and ignore the others.

Example results:

/         ==> /
/a        ==> /a
/a/       ==> /a
/a/b      ==> /a/b
/a/b/     ==> /a/b
/a/b/?c=d ==> /a/b?c=d
Ronen Teva
  • 1,345
  • 1
  • 23
  • 43
0

The answers above will work in a lot of cases but GET vars can encounter issues and if you were to put that inside another express middleware its reliance on req.path will cause a problem and its reliance on req.url can also have unwanted side effects. If you're looking for a tighter solution this will do the trick:

// Redirect non trailing slash to trailing slash
app.use(function(req, res, next){
    // Find the query string
    var qsi = req.originalUrl.indexOf('?');
    // Get the path
    var path = req.originalUrl;
    if(qsi > -1) path = path.substr(0, qsi);
    // Continue if the path is good or it's a static resource
    if(path.substr(-1) === '/' || ~path.indexOf('.')) return next();
    // Save just the query string
    var qs = '';
    if(qsi > -1) qs = req.originalUrl.substr(qsi);
    // Save redirect path
    var redirect = path + '/' + qs;
    // Redirect client
    res.redirect(301, redirect);

    console.log('301 redirected ' + req.originalUrl + ' to ' + redirect);
});

It's always happy with GET variables and won't break if you were to put it inside middleware.

Siyual
  • 16,415
  • 8
  • 44
  • 58
user2687646
  • 264
  • 1
  • 5
  • 12
0
/**
 * @param {express.Request} req
 * @param {express.Response} res
 * @param {express.NextFunction} next
 * @return {void}
 */
function checkTrailingSlash(req, res, next) {
    if (req.path.slice(req.path.length-1) !== '/') {
        res.redirect(301, req.path + '/' + req.url.slice(req.path.length));
    } else {
        next();
    }
}
  
app.use(checkTrailingSlash);

Example results:

/         ==> /
/a        ==> /a/
/a/       ==> /a/
/a/b      ==> /a/b/
/a/b/     ==> /a/b/
/a/b?c=d  ==> /a/b/?c=d
/a/b/?c=d ==> /a/b/?c=d
koc256
  • 36
  • 1
  • 3
0

If you use fastify to deal with your routes, you can try setting Fastify's ignoreTrailingSlash option to true.

const fastify = require('fastify')({
  ignoreTrailingSlash: true
})
Zitoun
  • 446
  • 3
  • 13