├── .gitignore ├── HISTORY.md ├── LICENSE ├── README.md ├── index.js ├── package.json └── test └── index.js /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | /coverage 3 | -------------------------------------------------------------------------------- /HISTORY.md: -------------------------------------------------------------------------------- 1 | Express Slash Change History 2 | ============================ 3 | 4 | 2.0.1 (2014-07-09) 5 | ------------------ 6 | 7 | * Fix bug with routers mounted with a `use()` path prefix. ([#5][] @awakenalive) 8 | 9 | [#5]: https://github.com/ericf/express-slash/issues/5 10 | 11 | 12 | 2.0.0 (2014-07-02) 13 | ------------------ 14 | 15 | * __[!]__ Added support for Express 4.x; this version does *not* work with 16 | Express 3.x anymore. ([#4][]) 17 | 18 | [#4]: https://github.com/ericf/express-slash/issues/4 19 | 20 | 21 | 1.0.1 (2014-07-02) 22 | ------------------ 23 | 24 | * Added tests. ([#3][] @balaclark) 25 | 26 | [#3]: https://github.com/ericf/express-slash/issues/3 27 | 28 | 29 | 1.0.0 (2014-01-03) 30 | ------------------ 31 | 32 | * __[!]__ Stable. 33 | 34 | * Defined a previously undefined `var`. 35 | 36 | 37 | 0.2.1 (2013-11-06) 38 | ------------------ 39 | 40 | * Add "modown" keyword to package.json 41 | 42 | 43 | 0.2.0 (2013-04-20) 44 | ------------------ 45 | 46 | * Changed the behavior of this middleware to be smarter about whether it should 47 | check for a route matching the new URL path that ends *with* or *without* a 48 | trailing slash. 49 | 50 | * Limited this middleware to only attempt to handle GET and HEAD HTTP requests. 51 | 52 | 53 | 0.1.0 (2013-03-09) 54 | ------------------ 55 | 56 | * Initial release. 57 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013 by Eric Ferraiuolo (eferraiuolo@gmail.com). All rights reserved. 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the "Software"), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 7 | the Software, and to permit persons to whom the Software is furnished to do so, 8 | subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 15 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 16 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 17 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 18 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 19 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Express Slash 2 | ============= 3 | 4 | [![Dependency Status](https://gemnasium.com/ericf/express-slash.png)](https://gemnasium.com/ericf/express-slash) 5 | [![npm Version](https://badge.fury.io/js/express-slash.png)](https://npmjs.org/package/express-slash) 6 | 7 | [Express][] middleware for people who are anal about trailing slashes. 8 | 9 | If you're a good person, then you enable Express' `"strict routing"` because 10 | you understand the difference between `/about` and `/about/`. You know that 11 | these URLs are not the same and they have different meanings. The trouble is, 12 | being a good person and caring about your trailing slashes is harder than not. 13 | Plus, you also care about other people, and it would be rude to 404 them when 14 | they forget the trailing slash. Luckily, there's this package to solve all your 15 | trailing slash problems :D 16 | 17 | **This Express middleware should come after your app's `router` middleware.** 18 | It will handle [GET and HEAD] requests for URLs which did not have a matching 19 | route by either adding or removing a trailing slash to the URL's path, then 20 | checking the app's router for a matching route for the new URL, in which case it 21 | will redirect the client (301 by default) to that URL. 22 | 23 | **Note:** Choose the correct version of this package for your Express version: 24 | 25 | * `v1.x`: Express 3.x 26 | * `v2.x`: Express 4.x 27 | 28 | 29 | [Express]: https://github.com/visionmedia/express 30 | 31 | 32 | Installation 33 | ------------ 34 | 35 | Install using npm: 36 | 37 | ```shell 38 | $ npm install express-slash 39 | ``` 40 | 41 | 42 | Usage 43 | ----- 44 | 45 | Enable Express' `"strict routing"` setting, and add this middleware after your 46 | app's `router` middleware: 47 | 48 | ```javascript 49 | var express = require('express'), 50 | slash = require('express-slash'); 51 | 52 | var app = express(); 53 | 54 | // Because you're the type of developer who cares about this sort of thing! 55 | app.enable('strict routing'); 56 | 57 | // Create the router using the same routing options as the app. 58 | var router = express.Router({ 59 | caseSensitive: app.get('case sensitive routing'), 60 | strict : app.get('strict routing') 61 | }); 62 | 63 | // Add the `slash()` middleware after your app's `router`, optionally specify 64 | // an HTTP status code to use when redirecting (defaults to 301). 65 | app.use(router); 66 | app.use(slash()); 67 | 68 | router.get('/', function (req, res) { 69 | res.send('Home'); 70 | }); 71 | 72 | router.get('/about/', function (req, res) { 73 | res.send('About'); 74 | }); 75 | 76 | router.get('/about/people', function (req, res) { 77 | res.send('People'); 78 | }); 79 | 80 | app.listen(3000); 81 | ``` 82 | 83 | Now when someone navigates to `/about`, they'll be redirected to `/about/`, and 84 | when someone navigates to `/about/people/`, they'll be redirected to 85 | `/about/people`. 86 | 87 | 88 | License 89 | ------- 90 | 91 | This software is free to use under the MIT license. 92 | See the [LICENSE file][] for license text and copyright information. 93 | 94 | 95 | [LICENSE file]: https://github.com/ericf/express-slash/blob/master/LICENSE 96 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var parseURL = require('url').parse; 4 | 5 | module.exports = expressSlash; 6 | 7 | // ----------------------------------------------------------------------------- 8 | 9 | function expressSlash(statusCode) { 10 | // Force a permanent redirect, unless otherwise specified. 11 | statusCode || (statusCode = 301); 12 | 13 | return function (req, res, next) { 14 | var method = req.method.toLowerCase(); 15 | 16 | // Skip when the request is neither a GET or HEAD. 17 | if (!(method === 'get' || method === 'head')) { 18 | next(); 19 | return; 20 | } 21 | 22 | var url = parseURL(req.url), 23 | pathname = url.pathname, 24 | search = url.search || '', 25 | hasSlash = pathname.charAt(pathname.length - 1) === '/'; 26 | 27 | // Adjust the URL's path by either adding or removing a trailing slash. 28 | pathname = hasSlash ? pathname.slice(0, -1) : (pathname + '/'); 29 | 30 | // Look for matching route. 31 | var match = testStackForMatch(req.app._router.stack, method, pathname); 32 | 33 | if (match) { 34 | res.redirect(statusCode, pathname + search); 35 | } else { 36 | next(); 37 | } 38 | }; 39 | } 40 | 41 | function testStackForMatch(stack, method, path) { 42 | return stack.some(function (layer) { 43 | var route = layer.route, 44 | subStack = layer.handle.stack; 45 | 46 | // It's only a match if the stack layer is a route. 47 | if (route) { 48 | return route.methods[method] && layer.match(path); 49 | } 50 | 51 | if (subStack) { 52 | // Trim any `.use()` prefix. 53 | if (layer.path) { 54 | path = trimPrefix(path, layer.path); 55 | } 56 | 57 | // Recurse into nested apps/routers. 58 | return testStackForMatch(subStack, method, path); 59 | } 60 | 61 | return false; 62 | }); 63 | } 64 | 65 | function trimPrefix(path, prefix) { 66 | var charAfterPrefix = path.charAt(prefix.length); 67 | 68 | if (charAfterPrefix === '/' || charAfterPrefix === '.') { 69 | path = path.substring(prefix.length); 70 | 71 | if (path.charAt(0) !== '/') { 72 | path = '/' + path; 73 | } 74 | } 75 | 76 | return path; 77 | } 78 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "express-slash", 3 | "description": "Express middleware for people who are anal about trailing slashes.", 4 | "version": "2.0.1", 5 | "homepage": "https://github.com/ericf/express-slash", 6 | "keywords": [ 7 | "express", 8 | "express3", 9 | "url", 10 | "urls", 11 | "slash", 12 | "trailing", 13 | "slashes", 14 | "route", 15 | "routes", 16 | "routing", 17 | "modown" 18 | ], 19 | "author": { 20 | "name": "Eric Ferraiuolo", 21 | "email": "eferraiuolo@gmail.com", 22 | "url": "http://ericf.me/" 23 | }, 24 | "contributors": [ 25 | { 26 | "name": "Bala Clark", 27 | "email": "balaclark@gmail.com", 28 | "url": "http://balaclark.com" 29 | } 30 | ], 31 | "repository": { 32 | "type": "git", 33 | "url": "git://github.com/ericf/express-slash.git" 34 | }, 35 | "bugs": { 36 | "url": "https://github.com/ericf/express-slash/issues" 37 | }, 38 | "dependencies": {}, 39 | "devDependencies": { 40 | "mocha": "^1.20.1", 41 | "istanbul": "^0.2.14", 42 | "express": "^4.4.5", 43 | "supertest": "^0.13.0" 44 | }, 45 | "main": "index", 46 | "scripts": { 47 | "test": "./node_modules/istanbul/lib/cli.js cover ./node_modules/mocha/bin/_mocha test -- --reporter spec" 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | /* global beforeEach, describe, it */ 2 | 'use strict'; 3 | 4 | var express = require('express'); 5 | var slash = require(__dirname + '/../'); 6 | var request = require('supertest'); 7 | 8 | describe('App', function () { 9 | var app; 10 | 11 | beforeEach(function () { 12 | var respond = function (req, res) { 13 | res.send('done.'); 14 | }; 15 | 16 | app = express(); 17 | 18 | app.enable('strict routing'); 19 | app.use(slash()); 20 | 21 | app.get('/slash/', respond); 22 | app.put('/slash/', respond); 23 | app.post('/slash/', respond); 24 | app.get('/noslash', respond); 25 | }); 26 | 27 | it('adds slashes when they are needed', function (done) { 28 | request(app) 29 | .get('/slash') 30 | .expect('location', '/slash/') 31 | .expect(301, done); 32 | }); 33 | 34 | it('removes slashes when they are not needed', function (done) { 35 | request(app) 36 | .get('/noslash/') 37 | .expect('location', '/noslash') 38 | .expect(301, done); 39 | }); 40 | 41 | it('only works with GET requests', function (done) { 42 | request(app).put('/slash').expect(404, function () { 43 | request(app).post('/slash').expect(404, done); 44 | }); 45 | }); 46 | 47 | it("doesn't do anything if the requested method doesn't have any routes", function (done) { 48 | request(app) 49 | .head('/slash') 50 | .expect(404, done); 51 | }); 52 | 53 | 54 | it("doesn't do anything if the requested route is correct", function (done) { 55 | request(app) 56 | .get('/slash/') 57 | .expect(200, done); 58 | }); 59 | }); 60 | 61 | describe('Router', function () { 62 | var app, router; 63 | 64 | beforeEach(function () { 65 | var respond = function (req, res) { 66 | res.send('done.'); 67 | }; 68 | 69 | app = express(); 70 | app.enable('strict routing'); 71 | 72 | router = express.Router({ 73 | strict: app.get('strict routing') 74 | }); 75 | 76 | app.use(router); 77 | app.use(slash()); 78 | 79 | router.get('/slash/', respond); 80 | router.put('/slash/', respond); 81 | router.post('/slash/', respond); 82 | router.get('/noslash', respond); 83 | }); 84 | 85 | it('adds slashes when they are needed', function (done) { 86 | request(app) 87 | .get('/slash') 88 | .expect('location', '/slash/') 89 | .expect(301, done); 90 | }); 91 | 92 | it('removes slashes when they are not needed', function (done) { 93 | request(app) 94 | .get('/noslash/') 95 | .expect('location', '/noslash') 96 | .expect(301, done); 97 | }); 98 | 99 | it('only works with GET requests', function (done) { 100 | request(app).put('/slash').expect(404, function () { 101 | request(app).post('/slash').expect(404, done); 102 | }); 103 | }); 104 | 105 | it("doesn't do anything if the requested method doesn't have any routes", function (done) { 106 | request(app) 107 | .head('/slash') 108 | .expect(404, done); 109 | }); 110 | 111 | 112 | it("doesn't do anything if the requested route is correct", function (done) { 113 | request(app) 114 | .get('/slash/') 115 | .expect(200, done); 116 | }); 117 | }); 118 | 119 | describe('Nested Router', function () { 120 | var app, router; 121 | 122 | beforeEach(function () { 123 | var respond = function (req, res) { 124 | res.send('done.'); 125 | }; 126 | 127 | app = express(); 128 | app.enable('strict routing'); 129 | 130 | router = express.Router({ 131 | strict: app.get('strict routing') 132 | }); 133 | 134 | router.get('/slash/', respond); 135 | router.put('/slash/', respond); 136 | router.post('/slash/', respond); 137 | router.get('/noslash', respond); 138 | 139 | app.use('/', router); 140 | app.use('/nested/', router); 141 | app.use(slash()); 142 | 143 | }); 144 | 145 | it('adds slashes when they are needed', function (done) { 146 | request(app) 147 | .get('/slash') 148 | .expect('location', '/slash/') 149 | .expect(301, done); 150 | }); 151 | 152 | it('removes slashes when they are not needed', function (done) { 153 | request(app) 154 | .get('/noslash/') 155 | .expect('location', '/noslash') 156 | .expect(301, done); 157 | }); 158 | 159 | it('only works with GET requests', function (done) { 160 | request(app).put('/slash').expect(404, function () { 161 | request(app).post('/slash').expect(404, done); 162 | }); 163 | }); 164 | 165 | it("doesn't do anything if the requested method doesn't have any routes", function (done) { 166 | request(app) 167 | .head('/slash') 168 | .expect(404, done); 169 | }); 170 | 171 | 172 | it("doesn't do anything if the requested route is correct", function (done) { 173 | request(app) 174 | .get('/slash/') 175 | .expect(200, done); 176 | }); 177 | 178 | it('adds slashes when they are needed (for nested routes)', function (done) { 179 | request(app) 180 | .get('/nested/slash') 181 | .expect('location', '/nested/slash/') 182 | .expect(301, done); 183 | }); 184 | 185 | it('removes slashes when they are not needed (for nested routes)', function (done) { 186 | request(app) 187 | .get('/nested/noslash/') 188 | .expect('location', '/nested/noslash') 189 | .expect(301, done); 190 | }); 191 | 192 | it('only works with GET requests (for nested routes)', function (done) { 193 | request(app).put('/nested/slash').expect(404, function () { 194 | request(app).post('/nested/slash').expect(404, done); 195 | }); 196 | }); 197 | 198 | it("doesn't do anything if the requested method doesn't have any routes (for nested routes)", function (done) { 199 | request(app) 200 | .head('/nested/slash') 201 | .expect(404, done); 202 | }); 203 | 204 | 205 | it("doesn't do anything if the requested route is correct (for nested routes)", function (done) { 206 | request(app) 207 | .get('/nested/slash/') 208 | .expect(200, done); 209 | }); 210 | }); 211 | --------------------------------------------------------------------------------