3 var assert = require('assert')
4 var Buffer = require('safe-buffer').Buffer
5 var express = require('..')
6 var path = require('path')
7 var request = require('supertest')
8 var utils = require('./support/utils')
10 var fixtures = path.join(__dirname, '/fixtures')
11 var relative = path.relative(process.cwd(), fixtures)
13 var skipRelative = ~relative.indexOf('..') || path.resolve(relative) === relative
15 describe('express.static()', function () {
16 describe('basic operations', function () {
18 this.app = createApp()
21 it('should require root path', function () {
22 assert.throws(express.static.bind(), /root path required/)
25 it('should require root path to be string', function () {
26 assert.throws(express.static.bind(null, 42), /root path.*string/)
29 it('should serve static files', function (done) {
32 .expect(200, '- groceries', done)
35 it('should support nesting', function (done) {
37 .get('/users/tobi.txt')
38 .expect(200, 'ferret', done)
41 it('should set Content-Type', function (done) {
44 .expect('Content-Type', 'text/plain; charset=UTF-8')
48 it('should set Last-Modified', function (done) {
51 .expect('Last-Modified', /\d{2} \w{3} \d{4}/)
55 it('should default max-age=0', function (done) {
58 .expect('Cache-Control', 'public, max-age=0')
62 it('should support urlencoded pathnames', function (done) {
64 .get('/%25%20of%20dogs.txt')
65 .expect(200, '20%', done)
68 it('should not choke on auth-looking URL', function (done) {
71 .expect(404, 'Not Found', done)
74 it('should support index.html', function (done) {
78 .expect('Content-Type', /html/)
79 .expect('<p>tobi, loki, jane</p>', done)
82 it('should support ../', function (done) {
84 .get('/users/../todo.txt')
85 .expect(200, '- groceries', done)
88 it('should support HEAD', function (done) {
92 .expect(utils.shouldNotHaveBody())
96 it('should skip POST requests', function (done) {
99 .expect(404, 'Not Found', done)
102 it('should support conditional requests', function (done) {
107 .end(function (err, res) {
111 .set('If-None-Match', res.headers.etag)
116 it('should support precondition checks', function (done) {
119 .set('If-Match', '"foo"')
123 it('should serve zero-length files', function (done) {
126 .expect(200, '', done)
129 it('should ignore hidden files', function (done) {
132 .expect(404, 'Not Found', done)
136 (skipRelative ? describe.skip : describe)('current dir', function () {
138 this.app = createApp('.')
141 it('should be served with "."', function (done) {
142 var dest = relative.split(path.sep).join('/')
144 .get('/' + dest + '/todo.txt')
145 .expect(200, '- groceries', done)
149 describe('acceptRanges', function () {
150 describe('when false', function () {
151 it('should not include Accept-Ranges', function (done) {
152 request(createApp(fixtures, { 'acceptRanges': false }))
154 .expect(utils.shouldNotHaveHeader('Accept-Ranges'))
155 .expect(200, '123456789', done)
158 it('should ignore Rage request header', function (done) {
159 request(createApp(fixtures, { 'acceptRanges': false }))
161 .set('Range', 'bytes=0-3')
162 .expect(utils.shouldNotHaveHeader('Accept-Ranges'))
163 .expect(utils.shouldNotHaveHeader('Content-Range'))
164 .expect(200, '123456789', done)
168 describe('when true', function () {
169 it('should include Accept-Ranges', function (done) {
170 request(createApp(fixtures, { 'acceptRanges': true }))
172 .expect('Accept-Ranges', 'bytes')
173 .expect(200, '123456789', done)
176 it('should obey Rage request header', function (done) {
177 request(createApp(fixtures, { 'acceptRanges': true }))
179 .set('Range', 'bytes=0-3')
180 .expect('Accept-Ranges', 'bytes')
181 .expect('Content-Range', 'bytes 0-3/9')
182 .expect(206, '1234', done)
187 describe('cacheControl', function () {
188 describe('when false', function () {
189 it('should not include Cache-Control', function (done) {
190 request(createApp(fixtures, { 'cacheControl': false }))
192 .expect(utils.shouldNotHaveHeader('Cache-Control'))
193 .expect(200, '123456789', done)
196 it('should ignore maxAge', function (done) {
197 request(createApp(fixtures, { 'cacheControl': false, 'maxAge': 12000 }))
199 .expect(utils.shouldNotHaveHeader('Cache-Control'))
200 .expect(200, '123456789', done)
204 describe('when true', function () {
205 it('should include Cache-Control', function (done) {
206 request(createApp(fixtures, { 'cacheControl': true }))
208 .expect('Cache-Control', 'public, max-age=0')
209 .expect(200, '123456789', done)
214 describe('extensions', function () {
215 it('should be not be enabled by default', function (done) {
216 request(createApp(fixtures))
221 it('should be configurable', function (done) {
222 request(createApp(fixtures, { 'extensions': 'txt' }))
224 .expect(200, '- groceries', done)
227 it('should support disabling extensions', function (done) {
228 request(createApp(fixtures, { 'extensions': false }))
233 it('should support fallbacks', function (done) {
234 request(createApp(fixtures, { 'extensions': ['htm', 'html', 'txt'] }))
236 .expect(200, '<li>groceries</li>', done)
239 it('should 404 if nothing found', function (done) {
240 request(createApp(fixtures, { 'extensions': ['htm', 'html', 'txt'] }))
246 describe('fallthrough', function () {
247 it('should default to true', function (done) {
249 .get('/does-not-exist')
250 .expect(404, 'Not Found', done)
253 describe('when true', function () {
255 this.app = createApp(fixtures, { 'fallthrough': true })
258 it('should fall-through when OPTIONS request', function (done) {
260 .options('/todo.txt')
261 .expect(404, 'Not Found', done)
264 it('should fall-through when URL malformed', function (done) {
267 .expect(404, 'Not Found', done)
270 it('should fall-through when traversing past root', function (done) {
272 .get('/users/../../todo.txt')
273 .expect(404, 'Not Found', done)
276 it('should fall-through when URL too long', function (done) {
278 var root = fixtures + Array(10000).join('/foobar')
280 app.use(express.static(root, { 'fallthrough': true }))
281 app.use(function (req, res, next) {
287 .expect(404, 'Not Found', done)
290 describe('with redirect: true', function () {
292 this.app = createApp(fixtures, { 'fallthrough': true, 'redirect': true })
295 it('should fall-through when directory', function (done) {
298 .expect(404, 'Not Found', done)
301 it('should redirect when directory without slash', function (done) {
304 .expect(301, /Redirecting/, done)
308 describe('with redirect: false', function () {
310 this.app = createApp(fixtures, { 'fallthrough': true, 'redirect': false })
313 it('should fall-through when directory', function (done) {
316 .expect(404, 'Not Found', done)
319 it('should fall-through when directory without slash', function (done) {
322 .expect(404, 'Not Found', done)
327 describe('when false', function () {
329 this.app = createApp(fixtures, { 'fallthrough': false })
332 it('should 405 when OPTIONS request', function (done) {
334 .options('/todo.txt')
335 .expect('Allow', 'GET, HEAD')
339 it('should 400 when URL malformed', function (done) {
342 .expect(400, /BadRequestError/, done)
345 it('should 403 when traversing past root', function (done) {
347 .get('/users/../../todo.txt')
348 .expect(403, /ForbiddenError/, done)
351 it('should 404 when URL too long', function (done) {
353 var root = fixtures + Array(10000).join('/foobar')
355 app.use(express.static(root, { 'fallthrough': false }))
356 app.use(function (req, res, next) {
362 .expect(404, /ENAMETOOLONG/, done)
365 describe('with redirect: true', function () {
367 this.app = createApp(fixtures, { 'fallthrough': false, 'redirect': true })
370 it('should 404 when directory', function (done) {
373 .expect(404, /NotFoundError|ENOENT/, done)
376 it('should redirect when directory without slash', function (done) {
379 .expect(301, /Redirecting/, done)
383 describe('with redirect: false', function () {
385 this.app = createApp(fixtures, { 'fallthrough': false, 'redirect': false })
388 it('should 404 when directory', function (done) {
391 .expect(404, /NotFoundError|ENOENT/, done)
394 it('should 404 when directory without slash', function (done) {
397 .expect(404, /NotFoundError|ENOENT/, done)
403 describe('hidden files', function () {
405 this.app = createApp(fixtures, { 'dotfiles': 'allow' })
408 it('should be served when dotfiles: "allow" is given', function (done) {
412 .expect(utils.shouldHaveBody(Buffer.from('tobi')))
417 describe('immutable', function () {
418 it('should default to false', function (done) {
419 request(createApp(fixtures))
421 .expect('Cache-Control', 'public, max-age=0', done)
424 it('should set immutable directive in Cache-Control', function (done) {
425 request(createApp(fixtures, { 'immutable': true, 'maxAge': '1h' }))
427 .expect('Cache-Control', 'public, max-age=3600, immutable', done)
431 describe('lastModified', function () {
432 describe('when false', function () {
433 it('should not include Last-Modified', function (done) {
434 request(createApp(fixtures, { 'lastModified': false }))
436 .expect(utils.shouldNotHaveHeader('Last-Modified'))
437 .expect(200, '123456789', done)
441 describe('when true', function () {
442 it('should include Last-Modified', function (done) {
443 request(createApp(fixtures, { 'lastModified': true }))
445 .expect('Last-Modified', /^\w{3}, \d+ \w+ \d+ \d+:\d+:\d+ \w+$/)
446 .expect(200, '123456789', done)
451 describe('maxAge', function () {
452 it('should accept string', function (done) {
453 request(createApp(fixtures, { 'maxAge': '30d' }))
455 .expect('cache-control', 'public, max-age=' + (60 * 60 * 24 * 30))
459 it('should be reasonable when infinite', function (done) {
460 request(createApp(fixtures, { 'maxAge': Infinity }))
462 .expect('cache-control', 'public, max-age=' + (60 * 60 * 24 * 365))
467 describe('redirect', function () {
470 this.app.use(function (req, res, next) {
471 req.originalUrl = req.url =
472 req.originalUrl.replace(/\/snow(\/|$)/, '/snow \u2603$1')
475 this.app.use(express.static(fixtures))
478 it('should redirect directories', function (done) {
481 .expect('Location', '/users/')
485 it('should include HTML link', function (done) {
488 .expect('Location', '/users/')
489 .expect(301, /<a href="\/users\/">/, done)
492 it('should redirect directories with query string', function (done) {
494 .get('/users?name=john')
495 .expect('Location', '/users/?name=john')
499 it('should not redirect to protocol-relative locations', function (done) {
502 .expect('Location', '/users/')
506 it('should ensure redirect URL is properly encoded', function (done) {
509 .expect('Location', '/snow%20%E2%98%83/')
510 .expect('Content-Type', /html/)
511 .expect(301, />Redirecting to <a href="\/snow%20%E2%98%83\/">\/snow%20%E2%98%83\/<\/a></, done)
514 it('should respond with default Content-Security-Policy', function (done) {
517 .expect('Content-Security-Policy', "default-src 'none'")
521 it('should not redirect incorrectly', function (done) {
527 describe('when false', function () {
529 this.app = createApp(fixtures, { 'redirect': false })
532 it('should disable redirect', function (done) {
540 describe('setHeaders', function () {
543 this.app.use(express.static(fixtures, { 'setHeaders': function (res) {
544 res.setHeader('x-custom', 'set')
548 it('should reject non-functions', function () {
549 assert.throws(express.static.bind(null, fixtures, { 'setHeaders': 3 }), /setHeaders.*function/)
552 it('should get called when sending file', function (done) {
555 .expect('x-custom', 'set')
559 it('should not get called on 404', function (done) {
562 .expect(utils.shouldNotHaveHeader('x-custom'))
566 it('should not get called on redirect', function (done) {
569 .expect(utils.shouldNotHaveHeader('x-custom'))
574 describe('when traversing past root', function () {
576 this.app = createApp(fixtures, { 'fallthrough': false })
579 it('should catch urlencoded ../', function (done) {
581 .get('/users/%2e%2e/%2e%2e/todo.txt')
585 it('should not allow root path disclosure', function (done) {
587 .get('/users/../../fixtures/todo.txt')
592 describe('when request has "Range" header', function () {
594 this.app = createApp()
597 it('should support byte ranges', function (done) {
600 .set('Range', 'bytes=0-4')
601 .expect('12345', done)
604 it('should be inclusive', function (done) {
607 .set('Range', 'bytes=0-0')
611 it('should set Content-Range', function (done) {
614 .set('Range', 'bytes=2-5')
615 .expect('Content-Range', 'bytes 2-5/9', done)
618 it('should support -n', function (done) {
621 .set('Range', 'bytes=-3')
625 it('should support n-', function (done) {
628 .set('Range', 'bytes=3-')
629 .expect('456789', done)
632 it('should respond with 206 "Partial Content"', function (done) {
635 .set('Range', 'bytes=0-4')
639 it('should set Content-Length to the # of octets transferred', function (done) {
642 .set('Range', 'bytes=2-3')
643 .expect('Content-Length', '2')
644 .expect(206, '34', done)
647 describe('when last-byte-pos of the range is greater than current length', function () {
648 it('is taken to be equal to one less than the current length', function (done) {
651 .set('Range', 'bytes=2-50')
652 .expect('Content-Range', 'bytes 2-8/9', done)
655 it('should adapt the Content-Length accordingly', function (done) {
658 .set('Range', 'bytes=2-50')
659 .expect('Content-Length', '7')
664 describe('when the first- byte-pos of the range is greater than the current length', function () {
665 it('should respond with 416', function (done) {
668 .set('Range', 'bytes=9-50')
672 it('should include a Content-Range header of complete length', function (done) {
675 .set('Range', 'bytes=9-50')
676 .expect('Content-Range', 'bytes */9')
681 describe('when syntactically invalid', function () {
682 it('should respond with 200 and the entire contents', function (done) {
685 .set('Range', 'asdf')
686 .expect('123456789', done)
691 describe('when index at mount point', function () {
694 this.app.use('/users', express.static(fixtures + '/users'))
697 it('should redirect correctly', function (done) {
700 .expect('Location', '/users/')
705 describe('when mounted', function () {
708 this.app.use('/static', express.static(fixtures))
711 it('should redirect relative to the originalUrl', function (done) {
713 .get('/static/users')
714 .expect('Location', '/static/users/')
718 it('should not choke on auth-looking URL', function (done) {
726 // NOTE: This is not a real part of the API, but
727 // over time this has become something users
728 // are doing, so this will prevent unseen
729 // regressions around this use-case.
731 describe('when mounted "root" as a file', function () {
734 this.app.use('/todo.txt', express.static(fixtures + '/todo.txt'))
737 it('should load the file when on trailing slash', function (done) {
740 .expect(200, '- groceries', done)
743 it('should 404 when trailing slash', function (done) {
750 describe('when responding non-2xx or 304', function () {
751 it('should not alter the status', function (done) {
754 app.use(function (req, res, next) {
758 app.use(express.static(fixtures))
762 .expect(501, '- groceries', done)
766 describe('when index file serving disabled', function () {
769 this.app.use('/static', express.static(fixtures, { 'index': false }))
770 this.app.use(function (req, res, next) {
775 it('should next() on directory', function (done) {
777 .get('/static/users/')
778 .expect(404, 'Not Found', done)
781 it('should redirect to trailing slash', function (done) {
783 .get('/static/users')
784 .expect('Location', '/static/users/')
788 it('should next() on mount point', function (done) {
791 .expect(404, 'Not Found', done)
794 it('should redirect to trailing slash mount point', function (done) {
797 .expect('Location', '/static/')
803 function createApp (dir, options, fn) {
805 var root = dir || fixtures
807 app.use(express.static(root, options))
809 app.use(function (req, res, next) {