Skip to content

Conversation

@dmgawel
Copy link

@dmgawel dmgawel commented Oct 1, 2025

Problem

When using the extensions option, send inconsistently handles path resolution depending on whether a directory exists at the requested path.

For example, given this file structure:

dist/
  about.html
  about/
    team.html
  contact.html

When requesting paths with extensions: ['html'] configured:

  • /contact (no directory exists) → serves contact.html
  • /about (directory exists) → redirects to /about/ instead of serving about.html

This inconsistency occurs because:

  1. When a path doesn't exist, send receives an ENOENT error and tries extensions, successfully serving contact.html
  2. When a path exists as a directory, send immediately calls redirect(path) without trying extensions first
  3. The extension checking logic only runs when the initial stat() returns ENOENT

This behavior is problematic for static site generators and frameworks that want to serve files like about.html from the URL /about (no trailing slash) while also having subdirectories like about/team.html.

Solution

Modified sendFile() to attempt extension resolution before redirecting when:

  • The path resolves to a directory
  • Extensions are configured
  • The path has no file extension
  • The path doesn't end with a separator

The next() function now tracks whether the original path was a directory via an isDir parameter. If all configured extensions fail to
resolve to a file and the original path was a directory, it falls back to the standard directory redirect behavior.

This ensures consistent behavior:

  • /contact with contact.html present → serves contact.html
  • /about with about.html present → serves about.html
  • /about with only about/ directory → redirects to /about/
  • /about with both present → serves about.html (prioritizes file over directory)

References

Resolves #194
Related to expressjs/serve-static#138

@UlisesGascon
Copy link
Member

Hey @dmgawel! Thanks for your PR... can you include also tests to prevent regressions in the future?

@dmgawel
Copy link
Author

dmgawel commented Dec 5, 2025

Hey @dmgawel! Thanks for your PR... can you include also tests to prevent regressions in the future?

@UlisesGascon Done ✅

Copy link
Member

@blakeembrey blakeembrey left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I feel like there is a breaking change here for a user that currently builds something like contact.html and contact/index.html, in that it'll change which page they're currently serving.

If we're already considering a breaking change for this, it might be worth considering how to simplify this entirely, such as should it go through all the extensions first? Would that also fix the issue for you?

I'm not sure familiar with the innards of the library so feel free to correct me if I'm misunderstanding.

if (stat.isDirectory()) return self.redirect(path)
if (stat.isDirectory()) {
// if extensions are configured and path has no extension, try extensions first
if (self._extensions.length > 0 && !extname(path) && !pathEndsWithSep) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do you need this condition at all? Wouldn't it be simpler to always call next(null, true) and it would already redirect if isDir === true?

The extname check feels like it could open a new issue along the same line, e.g. why can't it be contact.foo and contact.foo.html? Feels like it should act the same.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Allow falling back to file when directory exists but doesn't have index

3 participants