Skip to content

Commit 30fe8b1

Browse files
committed
Add fallthrough option
closes #33 closes #36
1 parent b5fb561 commit 30fe8b1

File tree

5 files changed

+264
-112
lines changed

5 files changed

+264
-112
lines changed

HISTORY.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
unreleased
22
==========
33

4+
* Add `fallthrough` option
5+
- Allows declaring this middleware is the final destination
6+
- Provides better integration with Express patterns
47
* Fix reading options from options prototype
58
* Improve the default redirect response headers
69

README.md

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,8 +41,8 @@ to "deny").
4141
The default value is `'ignore'`.
4242

4343
- `'allow'` No special treatment for dotfiles.
44-
- `'deny'` Send a 403 for any request for a dotfile.
45-
- `'ignore'` Pretend like the dotfile does not exist and call `next()`.
44+
- `'deny'` Deny a request for a dotfile and 403/`next()`.
45+
- `'ignore'` Pretend like the dotfile does not exist and 404/`next()`.
4646

4747
##### etag
4848

@@ -56,6 +56,25 @@ exists will be served. Example: `['html', 'htm']`.
5656

5757
The default value is `false`.
5858

59+
##### fallthrough
60+
61+
Set the middleware to have client errors fall-through as just unhandled
62+
requests, otherwise forward a client error. The difference is that client
63+
errors like a bad request or a request to a non-existent file will cause
64+
this middleware to simply `next()` to your next middleware when this value
65+
is `true`. When this value is `false`, these errors (even 404s), will invoke
66+
`next(err)`.
67+
68+
Typically `true` is desired such that multiple physical directories can be
69+
mapped to the same web address or for routes to fill in non-existent files.
70+
71+
The value `false` can be used if this middleware is mounted at a path that
72+
is designed to be strictly a single file system directory, which allows for
73+
short-circuiting 404s for less overhead. This middleware will also reply to
74+
all methods.
75+
76+
The default value is `true`.
77+
5978
##### index
6079

6180
By default this module will send "index.html" files in response to a request

index.js

Lines changed: 90 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,11 @@
1313
* @private
1414
*/
1515

16-
var escapeHtml = require('escape-html');
17-
var parseurl = require('parseurl');
18-
var resolve = require('path').resolve;
19-
var send = require('send');
20-
var url = require('url');
16+
var escapeHtml = require('escape-html')
17+
var parseUrl = require('parseurl')
18+
var resolve = require('path').resolve
19+
var send = require('send')
20+
var url = require('url')
2121

2222
/**
2323
* Module exports.
@@ -46,12 +46,14 @@ function serveStatic(root, options) {
4646
// copy options object
4747
var opts = Object.create(options || null)
4848

49+
// fall-though
50+
var fallthrough = opts.fallthrough !== false
51+
4952
// default redirect
5053
var redirect = opts.redirect !== false
5154

5255
// headers listener
5356
var setHeaders = opts.setHeaders
54-
opts.setHeaders = undefined
5557

5658
if (setHeaders && typeof setHeaders !== 'function') {
5759
throw new TypeError('option setHeaders must be function')
@@ -61,59 +63,61 @@ function serveStatic(root, options) {
6163
opts.maxage = opts.maxage || opts.maxAge || 0
6264
opts.root = resolve(root)
6365

66+
// construct directory listener
67+
var onDirectory = redirect
68+
? createRedirectDirectoryListener()
69+
: createNotFoundDirectoryListener()
70+
6471
return function serveStatic(req, res, next) {
6572
if (req.method !== 'GET' && req.method !== 'HEAD') {
66-
return next()
73+
if (fallthrough) {
74+
return next()
75+
}
76+
77+
// method not allowed
78+
res.statusCode = 405
79+
res.setHeader('Allow', 'GET, HEAD')
80+
res.setHeader('Content-Length', '0')
81+
res.end()
82+
return
6783
}
6884

69-
var originalUrl = parseurl.original(req)
70-
var path = parseurl(req).pathname
71-
var hasTrailingSlash = originalUrl.pathname[originalUrl.pathname.length - 1] === '/'
85+
var forwardError = !fallthrough
86+
var originalUrl = parseUrl.original(req)
87+
var path = parseUrl(req).pathname
7288

73-
if (path === '/' && !hasTrailingSlash) {
74-
// make sure redirect occurs at mount
89+
// make sure redirect occurs at mount
90+
if (path === '/' && originalUrl.pathname.substr(-1) !== '/') {
7591
path = ''
7692
}
7793

7894
// create send stream
7995
var stream = send(req, path, opts)
8096

81-
if (redirect) {
82-
// redirect relative to originalUrl
83-
stream.on('directory', function redirect() {
84-
if (hasTrailingSlash) {
85-
return next()
86-
}
87-
88-
// append trailing slash
89-
originalUrl.path = null
90-
originalUrl.pathname = collapseLeadingSlashes(originalUrl.pathname + '/')
91-
92-
// reformat the URL
93-
var loc = url.format(originalUrl)
94-
var msg = 'Redirecting to <a href="' + escapeHtml(loc) + '">' + escapeHtml(loc) + '</a>\n'
95-
96-
// send redirect response
97-
res.statusCode = 303
98-
res.setHeader('Content-Type', 'text/html; charset=UTF-8')
99-
res.setHeader('Content-Length', Buffer.byteLength(msg))
100-
res.setHeader('X-Content-Type-Options', 'nosniff')
101-
res.setHeader('Location', loc)
102-
res.end(msg)
103-
})
104-
} else {
105-
// forward to next middleware on directory
106-
stream.on('directory', next)
107-
}
97+
// add directory handler
98+
stream.on('directory', onDirectory)
10899

109100
// add headers listener
110101
if (setHeaders) {
111102
stream.on('headers', setHeaders)
112103
}
113104

114-
// forward non-404 errors
105+
// add file listener for fallthrough
106+
if (fallthrough) {
107+
stream.on('file', function onFile() {
108+
// once file is determined, always forward error
109+
forwardError = true
110+
})
111+
}
112+
113+
// forward errors
115114
stream.on('error', function error(err) {
116-
next(err.status === 404 ? null : err)
115+
if (forwardError || !(err.statusCode < 500)) {
116+
next(err)
117+
return
118+
}
119+
120+
next()
117121
})
118122

119123
// pipe
@@ -136,3 +140,48 @@ function collapseLeadingSlashes(str) {
136140
? '/' + str.substr(i)
137141
: str
138142
}
143+
144+
/**
145+
* Create a directory listener that just 404s.
146+
* @private
147+
*/
148+
149+
function createNotFoundDirectoryListener() {
150+
return function notFound() {
151+
this.error(404)
152+
}
153+
}
154+
155+
/**
156+
* Create a directory listener that performs a redirect.
157+
* @private
158+
*/
159+
160+
function createRedirectDirectoryListener() {
161+
return function redirect() {
162+
if (this.hasTrailingSlash()) {
163+
this.error(404)
164+
return
165+
}
166+
167+
// get original URL
168+
var originalUrl = parseUrl.original(this.req)
169+
170+
// append trailing slash
171+
originalUrl.path = null
172+
originalUrl.pathname = collapseLeadingSlashes(originalUrl.pathname + '/')
173+
174+
// reformat the URL
175+
var loc = url.format(originalUrl)
176+
var msg = 'Redirecting to <a href="' + escapeHtml(loc) + '">' + escapeHtml(loc) + '</a>\n'
177+
var res = this.res
178+
179+
// send redirect response
180+
res.statusCode = 303
181+
res.setHeader('Content-Type', 'text/html; charset=UTF-8')
182+
res.setHeader('Content-Length', Buffer.byteLength(msg))
183+
res.setHeader('X-Content-Type-Options', 'nosniff')
184+
res.setHeader('Location', loc)
185+
res.end(msg)
186+
}
187+
}

test/fixtures/pets/names.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
tobi,loki

0 commit comments

Comments
 (0)