5

How can I set whether the routing for express.static is case sensitive? e.g. whether Express should handle a request for image.jpeg by serving up a local file called Image.jpeg.

There is a caseSensitive option when calling express.Router([options]) (as defined at http://expressjs.com/en/4x/api.html) but this is not an option when calling express.static(root, [options]) (documentation at the same link).

By default, I get different behaviour serving static files from case insensitive volumes (/Mac OS X) to case sensitive volumes (/Linux). This results in inconsistent errors in our application - where something with a case mismatch works locally under Mac OS X but fails when deploying to the Linux server.

Alasdair McLeay
  • 2,572
  • 4
  • 27
  • 50
  • I would rather try to guarantee file path consistency there. If the requested file is 'image.jpeg', do not name it 'Image.jpeg'. Is there anything preventing you from doing this? – E_net4 Apr 25 '16 at 16:21
  • 3
    The intention is certainly that the case would match, but this inconsistency in behaviour means our infrastructure is vulnerable to typos/developer error and that issues are not picked up until code is deployed. I either want the code to error both locally and on the server, or succeed locally and on the server. – Alasdair McLeay Apr 25 '16 at 16:34
  • `serve-static` does not have an option to specify case sensitivity (which could be tricky to achieve on some platforms, as you've mentioned). Your best bet would be making integration tests that cover your server's process of serving static files. – E_net4 Apr 25 '16 at 16:45

3 Answers3

2

I wanted this too, so I knocked up a quick way to 404 requests with mismatching case.

It is not particularly efficient, so I only run it in development. It only checks the filename. It doesn't check the case of the folders above the file.

How to use it:

var express = require('express');

var app = express();
// You can do this before or after the express() call
// But it must come before express.static() is called
var inDevelopment = (process.NODE_ENV || 'local') === 'local';
if (inDevelopment) {
    require('./modules/makeExpressStaticCaseSensitive')(express);
}

app.use(express.static(path.join(__dirname, 'public_html')));

The script module/makeExpressStaticCaseSensitive.js

module.exports = function (express) {
    var fs = require('fs')
    var pathlib = require('path');
    var parseUrl = require('express/node_modules/parseurl')

    var oldStatic = express.static;
    var newStatic = function (root, options) {
        var opts = Object.create(options || null);

        var originalHandler = oldStatic(root, options);

        var wrappedHandler = function (req, res, next) {
            var filepath = pathlib.join(root, parseUrl(req).pathname);
            var dirpath = pathlib.dirname(filepath);
            var filename = pathlib.basename(filepath);

            // @todo Reading the entire directory listing and then searching it is quite inefficient for large folders
            //       We should find a more efficient way to do this for one file at a time
            fs.readdir(dirpath, function (err, files) {
                if (err) return next(err);

                var fileIsThere = files.indexOf(filename) >= 0;
                if (fileIsThere) {
                    originalHandler(req, res, next);
                } else {
                    res.status(404).end();
                }
            });
        };

        return wrappedHandler;
    };
    express.static = newStatic;
};

I have written a more efficient version which caches the output of readdir() for a few seconds, and check the entire path, but it is somewhat longer.

joeytwiddle
  • 29,306
  • 13
  • 121
  • 110
1

I was given also some advice for a simple alternative solution:

Set a policy that all your static files and paths should be in lowercase.

Enforce the policy on the files by adding a build step that checks them.

Enforce the policy on requests by adding some middleware that rejects requests to the static folder if any uppercase characters are found in the path.

joeytwiddle
  • 29,306
  • 13
  • 121
  • 110
1

I encountered this issue recently and did a debug into the underlying code for express.static. I could not find a solution for the scenario except to make the naming consistent. But I thought it will be a good idea to share what is happening under the hoods. There is no option to customize the behavior and the case sensitivity is determined by the host filesystem (MacOS APFS that defaulted to case sensitive for me).

express.static is actually using the npm package serve-static and serve-static uses the npm package send to return the file.

And send actually uses Node.js's fs.stat to check that the file exists before returning the file. And the case sensitivity of that call depends on the host filesystem of Express.js. For host filesystems that are case sensitive the call will return and error of the file not being found.

This is an extract of the code that is causing the issue

  fs.stat(path, function onstat (err, stat) {
    if (err && err.code === 'ENOENT' && !extname(path) && path[path.length - 1] !== sep) {
      // not found, check extensions
      return next(err)
    }
    if (err) return self.onStatError(err)
    if (stat.isDirectory()) return self.redirect(path)
    self.emit('file', path, stat)
    self.send(path, stat)
  })

I do not locate any options that can be passed into it from Express that allows the behavior to be customized. express.static also performs a lot of important features like setting of response headers (caching, etc) for the file so removing it without an equivalent substitute is also not a good solution.

seetdev
  • 577
  • 1
  • 5
  • 11