test/express.static.js JAVASCRIPT 816 lines View on github.com → Search inside
1'use strict'23var assert = require('node:assert')4var express = require('..')5var path = require('node:path')6const { Buffer } = require('node:buffer');78var request = require('supertest')9var utils = require('./support/utils')1011var fixtures = path.join(__dirname, '/fixtures')12var relative = path.relative(process.cwd(), fixtures)1314var skipRelative = ~relative.indexOf('..') || path.resolve(relative) === relative1516describe('express.static()', function () {17  describe('basic operations', function () {18    before(function () {19      this.app = createApp()20    })2122    it('should require root path', function () {23      assert.throws(express.static.bind(), /root path required/)24    })2526    it('should require root path to be string', function () {27      assert.throws(express.static.bind(null, 42), /root path.*string/)28    })2930    it('should serve static files', function (done) {31      request(this.app)32        .get('/todo.txt')33        .expect(200, '- groceries', done)34    })3536    it('should support nesting', function (done) {37      request(this.app)38        .get('/users/tobi.txt')39        .expect(200, 'ferret', done)40    })4142    it('should set Content-Type', function (done) {43      request(this.app)44        .get('/todo.txt')45        .expect('Content-Type', 'text/plain; charset=utf-8')46        .expect(200, done)47    })4849    it('should set Last-Modified', function (done) {50      request(this.app)51        .get('/todo.txt')52        .expect('Last-Modified', /\d{2} \w{3} \d{4}/)53        .expect(200, done)54    })5556    it('should default max-age=0', function (done) {57      request(this.app)58        .get('/todo.txt')59        .expect('Cache-Control', 'public, max-age=0')60        .expect(200, done)61    })6263    it('should support urlencoded pathnames', function (done) {64      request(this.app)65        .get('/%25%20of%20dogs.txt')66        .expect(200, '20%', done)67    })6869    it('should not choke on auth-looking URL', function (done) {70      request(this.app)71        .get('//todo@txt')72        .expect(404, 'Not Found', done)73    })7475    it('should support index.html', function (done) {76      request(this.app)77        .get('/users/')78        .expect(200)79        .expect('Content-Type', /html/)80        .expect('<p>tobi, loki, jane</p>', done)81    })8283    it('should support ../', function (done) {84      request(this.app)85        .get('/users/../todo.txt')86        .expect(200, '- groceries', done)87    })8889    it('should support HEAD', function (done) {90      request(this.app)91        .head('/todo.txt')92        .expect(200)93        .expect(utils.shouldNotHaveBody())94        .end(done)95    })9697    it('should skip POST requests', function (done) {98      request(this.app)99        .post('/todo.txt')100        .expect(404, 'Not Found', done)101    })102103    it('should support conditional requests', function (done) {104      var app = this.app105106      request(app)107        .get('/todo.txt')108        .end(function (err, res) {109          if (err) throw err110          request(app)111            .get('/todo.txt')112            .set('If-None-Match', res.headers.etag)113            .expect(304, done)114        })115    })116117    it('should support precondition checks', function (done) {118      request(this.app)119        .get('/todo.txt')120        .set('If-Match', '"foo"')121        .expect(412, done)122    })123124    it('should serve zero-length files', function (done) {125      request(this.app)126        .get('/empty.txt')127        .expect(200, '', done)128    })129130    it('should ignore hidden files', function (done) {131      request(this.app)132        .get('/.name')133        .expect(404, 'Not Found', done)134    })135  });136137  (skipRelative ? describe.skip : describe)('current dir', function () {138    before(function () {139      this.app = createApp('.')140    })141142    it('should be served with "."', function (done) {143      var dest = relative.split(path.sep).join('/')144      request(this.app)145        .get('/' + dest + '/todo.txt')146        .expect(200, '- groceries', done)147    })148  })149150  describe('acceptRanges', function () {151    describe('when false', function () {152      it('should not include Accept-Ranges', function (done) {153        request(createApp(fixtures, { 'acceptRanges': false }))154          .get('/nums.txt')155          .expect(utils.shouldNotHaveHeader('Accept-Ranges'))156          .expect(200, '123456789', done)157      })158159      it('should ignore Rage request header', function (done) {160        request(createApp(fixtures, { 'acceptRanges': false }))161          .get('/nums.txt')162          .set('Range', 'bytes=0-3')163          .expect(utils.shouldNotHaveHeader('Accept-Ranges'))164          .expect(utils.shouldNotHaveHeader('Content-Range'))165          .expect(200, '123456789', done)166      })167    })168169    describe('when true', function () {170      it('should include Accept-Ranges', function (done) {171        request(createApp(fixtures, { 'acceptRanges': true }))172          .get('/nums.txt')173          .expect('Accept-Ranges', 'bytes')174          .expect(200, '123456789', done)175      })176177      it('should obey Rage request header', function (done) {178        request(createApp(fixtures, { 'acceptRanges': true }))179          .get('/nums.txt')180          .set('Range', 'bytes=0-3')181          .expect('Accept-Ranges', 'bytes')182          .expect('Content-Range', 'bytes 0-3/9')183          .expect(206, '1234', done)184      })185    })186  })187188  describe('cacheControl', function () {189    describe('when false', function () {190      it('should not include Cache-Control', function (done) {191        request(createApp(fixtures, { 'cacheControl': false }))192          .get('/nums.txt')193          .expect(utils.shouldNotHaveHeader('Cache-Control'))194          .expect(200, '123456789', done)195      })196197      it('should ignore maxAge', function (done) {198        request(createApp(fixtures, { 'cacheControl': false, 'maxAge': 12000 }))199          .get('/nums.txt')200          .expect(utils.shouldNotHaveHeader('Cache-Control'))201          .expect(200, '123456789', done)202      })203    })204205    describe('when true', function () {206      it('should include Cache-Control', function (done) {207        request(createApp(fixtures, { 'cacheControl': true }))208          .get('/nums.txt')209          .expect('Cache-Control', 'public, max-age=0')210          .expect(200, '123456789', done)211      })212    })213  })214215  describe('extensions', function () {216    it('should be not be enabled by default', function (done) {217      request(createApp(fixtures))218        .get('/todo')219        .expect(404, done)220    })221222    it('should be configurable', function (done) {223      request(createApp(fixtures, { 'extensions': 'txt' }))224        .get('/todo')225        .expect(200, '- groceries', done)226    })227228    it('should support disabling extensions', function (done) {229      request(createApp(fixtures, { 'extensions': false }))230        .get('/todo')231        .expect(404, done)232    })233234    it('should support fallbacks', function (done) {235      request(createApp(fixtures, { 'extensions': ['htm', 'html', 'txt'] }))236        .get('/todo')237        .expect(200, '<li>groceries</li>', done)238    })239240    it('should 404 if nothing found', function (done) {241      request(createApp(fixtures, { 'extensions': ['htm', 'html', 'txt'] }))242        .get('/bob')243        .expect(404, done)244    })245  })246247  describe('fallthrough', function () {248    it('should default to true', function (done) {249      request(createApp())250        .get('/does-not-exist')251        .expect(404, 'Not Found', done)252    })253254    describe('when true', function () {255      before(function () {256        this.app = createApp(fixtures, { 'fallthrough': true })257      })258259      it('should fall-through when OPTIONS request', function (done) {260        request(this.app)261          .options('/todo.txt')262          .expect(404, 'Not Found', done)263      })264265      it('should fall-through when URL malformed', function (done) {266        request(this.app)267          .get('/%')268          .expect(404, 'Not Found', done)269      })270271      it('should fall-through when traversing past root', function (done) {272        request(this.app)273          .get('/users/../../todo.txt')274          .expect(404, 'Not Found', done)275      })276277      it('should fall-through when URL too long', function (done) {278        var app = express()279        var root = fixtures + Array(10000).join('/foobar')280281        app.use(express.static(root, { 'fallthrough': true }))282        app.use(function (req, res, next) {283          res.sendStatus(404)284        })285286        request(app)287          .get('/')288          .expect(404, 'Not Found', done)289      })290291      describe('with redirect: true', function () {292        before(function () {293          this.app = createApp(fixtures, { 'fallthrough': true, 'redirect': true })294        })295296        it('should fall-through when directory', function (done) {297          request(this.app)298            .get('/pets/')299            .expect(404, 'Not Found', done)300        })301302        it('should redirect when directory without slash', function (done) {303          request(this.app)304            .get('/pets')305            .expect(301, /Redirecting/, done)306        })307      })308309      describe('with redirect: false', function () {310        before(function () {311          this.app = createApp(fixtures, { 'fallthrough': true, 'redirect': false })312        })313314        it('should fall-through when directory', function (done) {315          request(this.app)316            .get('/pets/')317            .expect(404, 'Not Found', done)318        })319320        it('should fall-through when directory without slash', function (done) {321          request(this.app)322            .get('/pets')323            .expect(404, 'Not Found', done)324        })325      })326    })327328    describe('when false', function () {329      before(function () {330        this.app = createApp(fixtures, { 'fallthrough': false })331      })332333      it('should 405 when OPTIONS request', function (done) {334        request(this.app)335          .options('/todo.txt')336          .expect('Allow', 'GET, HEAD')337          .expect(405, done)338      })339340      it('should 400 when URL malformed', function (done) {341        request(this.app)342          .get('/%')343          .expect(400, /BadRequestError/, done)344      })345346      it('should 403 when traversing past root', function (done) {347        request(this.app)348          .get('/users/../../todo.txt')349          .expect(403, /ForbiddenError/, done)350      })351352      it('should 404 when URL too long', function (done) {353        var app = express()354        var root = fixtures + Array(10000).join('/foobar')355356        app.use(express.static(root, { 'fallthrough': false }))357        app.use(function (req, res, next) {358          res.sendStatus(404)359        })360361        request(app)362          .get('/')363          .expect(404, /ENAMETOOLONG/, done)364      })365366      describe('with redirect: true', function () {367        before(function () {368          this.app = createApp(fixtures, { 'fallthrough': false, 'redirect': true })369        })370371        it('should 404 when directory', function (done) {372          request(this.app)373            .get('/pets/')374            .expect(404, /NotFoundError|ENOENT/, done)375        })376377        it('should redirect when directory without slash', function (done) {378          request(this.app)379            .get('/pets')380            .expect(301, /Redirecting/, done)381        })382      })383384      describe('with redirect: false', function () {385        before(function () {386          this.app = createApp(fixtures, { 'fallthrough': false, 'redirect': false })387        })388389        it('should 404 when directory', function (done) {390          request(this.app)391            .get('/pets/')392            .expect(404, /NotFoundError|ENOENT/, done)393        })394395        it('should 404 when directory without slash', function (done) {396          request(this.app)397            .get('/pets')398            .expect(404, /NotFoundError|ENOENT/, done)399        })400      })401    })402  })403404  describe('hidden files', function () {405    before(function () {406      this.app = createApp(fixtures, { 'dotfiles': 'allow' })407    })408409    it('should be served when dotfiles: "allow" is given', function (done) {410      request(this.app)411        .get('/.name')412        .expect(200)413        .expect(utils.shouldHaveBody(Buffer.from('tobi')))414        .end(done)415    })416  })417418  describe('immutable', function () {419    it('should default to false', function (done) {420      request(createApp(fixtures))421        .get('/nums.txt')422        .expect('Cache-Control', 'public, max-age=0', done)423    })424425    it('should set immutable directive in Cache-Control', function (done) {426      request(createApp(fixtures, { 'immutable': true, 'maxAge': '1h' }))427        .get('/nums.txt')428        .expect('Cache-Control', 'public, max-age=3600, immutable', done)429    })430  })431432  describe('lastModified', function () {433    describe('when false', function () {434      it('should not include Last-Modified', function (done) {435        request(createApp(fixtures, { 'lastModified': false }))436          .get('/nums.txt')437          .expect(utils.shouldNotHaveHeader('Last-Modified'))438          .expect(200, '123456789', done)439      })440    })441442    describe('when true', function () {443      it('should include Last-Modified', function (done) {444        request(createApp(fixtures, { 'lastModified': true }))445          .get('/nums.txt')446          .expect('Last-Modified', /^\w{3}, \d+ \w+ \d+ \d+:\d+:\d+ \w+$/)447          .expect(200, '123456789', done)448      })449    })450  })451452  describe('maxAge', function () {453    it('should accept string', function (done) {454      request(createApp(fixtures, { 'maxAge': '30d' }))455        .get('/todo.txt')456        .expect('cache-control', 'public, max-age=' + (60 * 60 * 24 * 30))457        .expect(200, done)458    })459460    it('should be reasonable when infinite', function (done) {461      request(createApp(fixtures, { 'maxAge': Infinity }))462        .get('/todo.txt')463        .expect('cache-control', 'public, max-age=' + (60 * 60 * 24 * 365))464        .expect(200, done)465    })466  })467468  describe('redirect', function () {469    before(function () {470      this.app = express()471      this.app.use(function (req, res, next) {472        req.originalUrl = req.url =473          req.originalUrl.replace(/\/snow(\/|$)/, '/snow \u2603$1')474        next()475      })476      this.app.use(express.static(fixtures))477    })478479    it('should redirect directories', function (done) {480      request(this.app)481        .get('/users')482        .expect('Location', '/users/')483        .expect(301, done)484    })485486    it('should include HTML link', function (done) {487      request(this.app)488        .get('/users')489        .expect('Location', '/users/')490        .expect(301, /\/users\//, done)491    })492493    it('should redirect directories with query string', function (done) {494      request(this.app)495        .get('/users?name=john')496        .expect('Location', '/users/?name=john')497        .expect(301, done)498    })499500    it('should not redirect to protocol-relative locations', function (done) {501      request(this.app)502        .get('//users')503        .expect('Location', '/users/')504        .expect(301, done)505    })506507    it('should ensure redirect URL is properly encoded', function (done) {508      request(this.app)509        .get('/snow')510        .expect('Location', '/snow%20%E2%98%83/')511        .expect('Content-Type', /html/)512        .expect(301, />Redirecting to \/snow%20%E2%98%83\/</, done)513    })514515    it('should respond with default Content-Security-Policy', function (done) {516      request(this.app)517        .get('/users')518        .expect('Content-Security-Policy', "default-src 'none'")519        .expect(301, done)520    })521522    it('should not redirect incorrectly', function (done) {523      request(this.app)524        .get('/')525        .expect(404, done)526    })527528    describe('when false', function () {529      before(function () {530        this.app = createApp(fixtures, { 'redirect': false })531      })532533      it('should disable redirect', function (done) {534        request(this.app)535          .get('/users')536          .expect(404, done)537      })538    })539  })540541  describe('setHeaders', function () {542    before(function () {543      this.app = express()544      this.app.use(express.static(fixtures, { 'setHeaders': function (res) {545        res.setHeader('x-custom', 'set')546      } }))547    })548549    it('should reject non-functions', function () {550      assert.throws(express.static.bind(null, fixtures, { 'setHeaders': 3 }), /setHeaders.*function/)551    })552553    it('should get called when sending file', function (done) {554      request(this.app)555        .get('/nums.txt')556        .expect('x-custom', 'set')557        .expect(200, done)558    })559560    it('should not get called on 404', function (done) {561      request(this.app)562        .get('/bogus')563        .expect(utils.shouldNotHaveHeader('x-custom'))564        .expect(404, done)565    })566567    it('should not get called on redirect', function (done) {568      request(this.app)569        .get('/users')570        .expect(utils.shouldNotHaveHeader('x-custom'))571        .expect(301, done)572    })573  })574575  describe('when traversing past root', function () {576    before(function () {577      this.app = createApp(fixtures, { 'fallthrough': false })578    })579580    it('should catch urlencoded ../', function (done) {581      request(this.app)582        .get('/users/%2e%2e/%2e%2e/todo.txt')583        .expect(403, done)584    })585586    it('should not allow root path disclosure', function (done) {587      request(this.app)588        .get('/users/../../fixtures/todo.txt')589        .expect(403, done)590    })591  })592593  describe('when request has "Range" header', function () {594    before(function () {595      this.app = createApp()596    })597598    it('should support byte ranges', function (done) {599      request(this.app)600        .get('/nums.txt')601        .set('Range', 'bytes=0-4')602        .expect('12345', done)603    })604605    it('should be inclusive', function (done) {606      request(this.app)607        .get('/nums.txt')608        .set('Range', 'bytes=0-0')609        .expect('1', done)610    })611612    it('should set Content-Range', function (done) {613      request(this.app)614        .get('/nums.txt')615        .set('Range', 'bytes=2-5')616        .expect('Content-Range', 'bytes 2-5/9', done)617    })618619    it('should support -n', function (done) {620      request(this.app)621        .get('/nums.txt')622        .set('Range', 'bytes=-3')623        .expect('789', done)624    })625626    it('should support n-', function (done) {627      request(this.app)628        .get('/nums.txt')629        .set('Range', 'bytes=3-')630        .expect('456789', done)631    })632633    it('should respond with 206 "Partial Content"', function (done) {634      request(this.app)635        .get('/nums.txt')636        .set('Range', 'bytes=0-4')637        .expect(206, done)638    })639640    it('should set Content-Length to the # of octets transferred', function (done) {641      request(this.app)642        .get('/nums.txt')643        .set('Range', 'bytes=2-3')644        .expect('Content-Length', '2')645        .expect(206, '34', done)646    })647648    describe('when last-byte-pos of the range is greater than current length', function () {649      it('is taken to be equal to one less than the current length', function (done) {650        request(this.app)651          .get('/nums.txt')652          .set('Range', 'bytes=2-50')653          .expect('Content-Range', 'bytes 2-8/9', done)654      })655656      it('should adapt the Content-Length accordingly', function (done) {657        request(this.app)658          .get('/nums.txt')659          .set('Range', 'bytes=2-50')660          .expect('Content-Length', '7')661          .expect(206, done)662      })663    })664665    describe('when the first- byte-pos of the range is greater than the current length', function () {666      it('should respond with 416', function (done) {667        request(this.app)668          .get('/nums.txt')669          .set('Range', 'bytes=9-50')670          .expect(416, done)671      })672673      it('should include a Content-Range header of complete length', function (done) {674        request(this.app)675          .get('/nums.txt')676          .set('Range', 'bytes=9-50')677          .expect('Content-Range', 'bytes */9')678          .expect(416, done)679      })680    })681682    describe('when syntactically invalid', function () {683      it('should respond with 200 and the entire contents', function (done) {684        request(this.app)685          .get('/nums.txt')686          .set('Range', 'asdf')687          .expect('123456789', done)688      })689    })690  })691692  describe('when index at mount point', function () {693    before(function () {694      this.app = express()695      this.app.use('/users', express.static(fixtures + '/users'))696    })697698    it('should redirect correctly', function (done) {699      request(this.app)700        .get('/users')701        .expect('Location', '/users/')702        .expect(301, done)703    })704  })705706  describe('when mounted', function () {707    before(function () {708      this.app = express()709      this.app.use('/static', express.static(fixtures))710    })711712    it('should redirect relative to the originalUrl', function (done) {713      request(this.app)714        .get('/static/users')715        .expect('Location', '/static/users/')716        .expect(301, done)717    })718719    it('should not choke on auth-looking URL', function (done) {720      request(this.app)721        .get('//todo@txt')722        .expect(404, done)723    })724  })725726  //727  // NOTE: This is not a real part of the API, but728  //       over time this has become something users729  //       are doing, so this will prevent unseen730  //       regressions around this use-case.731  //732  describe('when mounted "root" as a file', function () {733    before(function () {734      this.app = express()735      this.app.use('/todo.txt', express.static(fixtures + '/todo.txt'))736    })737738    it('should load the file when on trailing slash', function (done) {739      request(this.app)740        .get('/todo.txt')741        .expect(200, '- groceries', done)742    })743744    it('should 404 when trailing slash', function (done) {745      request(this.app)746        .get('/todo.txt/')747        .expect(404, done)748    })749  })750751  describe('when responding non-2xx or 304', function () {752    it('should not alter the status', function (done) {753      var app = express()754755      app.use(function (req, res, next) {756        res.status(501)757        next()758      })759      app.use(express.static(fixtures))760761      request(app)762        .get('/todo.txt')763        .expect(501, '- groceries', done)764    })765  })766767  describe('when index file serving disabled', function () {768    before(function () {769      this.app = express()770      this.app.use('/static', express.static(fixtures, { 'index': false }))771      this.app.use(function (req, res, next) {772        res.sendStatus(404)773      })774    })775776    it('should next() on directory', function (done) {777      request(this.app)778        .get('/static/users/')779        .expect(404, 'Not Found', done)780    })781782    it('should redirect to trailing slash', function (done) {783      request(this.app)784        .get('/static/users')785        .expect('Location', '/static/users/')786        .expect(301, done)787    })788789    it('should next() on mount point', function (done) {790      request(this.app)791        .get('/static/')792        .expect(404, 'Not Found', done)793    })794795    it('should redirect to trailing slash mount point', function (done) {796      request(this.app)797        .get('/static')798        .expect('Location', '/static/')799        .expect(301, done)800    })801  })802})803804function createApp (dir, options, fn) {805  var app = express()806  var root = dir || fixtures807808  app.use(express.static(root, options))809810  app.use(function (req, res, next) {811    res.sendStatus(404)812  })813814  return app815}

Code quality findings 18

Use let or const to avoid scope issues and hoisting
info correctness var-declaration
var assert = require('node:assert')
Use let or const to avoid scope issues and hoisting
info correctness var-declaration
var express = require('..')
Use let or const to avoid scope issues and hoisting
info correctness var-declaration
var path = require('node:path')
Use let or const to avoid scope issues and hoisting
info correctness var-declaration
var request = require('supertest')
Use let or const to avoid scope issues and hoisting
info correctness var-declaration
var utils = require('./support/utils')
Use let or const to avoid scope issues and hoisting
info correctness var-declaration
var fixtures = path.join(__dirname, '/fixtures')
Use let or const to avoid scope issues and hoisting
info correctness var-declaration
var relative = path.relative(process.cwd(), fixtures)
Use let or const to avoid scope issues and hoisting
info correctness var-declaration
var skipRelative = ~relative.indexOf('..') || path.resolve(relative) === relative
Use strict equality (===) to prevent type coercion bugs
info correctness loose-equality
var skipRelative = ~relative.indexOf('..') || path.resolve(relative) === relative
Use let or const to avoid scope issues and hoisting
info correctness var-declaration
var app = this.app
Use let or const to avoid scope issues and hoisting
info correctness var-declaration
var dest = relative.split(path.sep).join('/')
Use let or const to avoid scope issues and hoisting
info correctness var-declaration
var app = express()
Use let or const to avoid scope issues and hoisting
info correctness var-declaration
var root = fixtures + Array(10000).join('/foobar')
Use let or const to avoid scope issues and hoisting
info correctness var-declaration
var app = express()
Use let or const to avoid scope issues and hoisting
info correctness var-declaration
var root = fixtures + Array(10000).join('/foobar')
Use let or const to avoid scope issues and hoisting
info correctness var-declaration
var app = express()
Use let or const to avoid scope issues and hoisting
info correctness var-declaration
var app = express()
Use let or const to avoid scope issues and hoisting
info correctness var-declaration
var root = dir || fixtures

Get this view in your editor

Same data, no extra tab — call code_get_file + code_get_findings over MCP from Claude/Cursor/Copilot.