Skip to content

fix(services)!: reject trailing slash for file paths#678

Open
Isvane wants to merge 5 commits into
tower-rs:mainfrom
Isvane:fix/trailing_slash
Open

fix(services)!: reject trailing slash for file paths#678
Isvane wants to merge 5 commits into
tower-rs:mainfrom
Isvane:fix/trailing_slash

Conversation

@Isvane
Copy link
Copy Markdown

@Isvane Isvane commented May 12, 2026

BREAKING CHANGE: Requests to file paths with a trailing slash (e.g., /index.html/)
will now correctly return a 404 FileNotFound instead of serving the file.
This alters previous behavior where the trailing slash was ignored.

Motivation

Hi. This is my first time contributing to global library and I wanted to help out with #654.

The goal here was to make sure that if someone tries to access a file with a trailing slash (like /index.html/), the server doesn't get confused and try to serve it as a file. It should only allow the trailing slash if it’s actually a directory.

Solution

  • In serve_dir/mod.rs, I added a check to see if the requested URI ends with a /.
  • I passed that info into the open_file function.
  • Inside open_file.rs, I added a check: if there’s a trailing slash but the metadata says "this is a file," we return FileNotFound.
  • I made sure this doesn't break the way the it looks for index.html inside real folders.
  • I added a new test case in tests.rs to make sure requesting /index.html/ now correctly returns a 404.

I'm very open to feedback if there’s a cleaner way to handle this or if I missed any project conventions, please let me know.

@Isvane Isvane marked this pull request as draft May 12, 2026 15:15
@Isvane Isvane marked this pull request as ready for review May 12, 2026 16:12
@jlizen
Copy link
Copy Markdown
Member

jlizen commented May 12, 2026

@jplatte this feels like probably the right default, but something that needs to ship across a breaking semver release due to the behavioral change, would you agree? One could call it a bugfix, but that seems a bit sketchy to me.

(specific PR code aside, I will review this more closely)

@jplatte
Copy link
Copy Markdown
Member

jplatte commented May 13, 2026

Yeah, seems likely enough that some users are currently (accidentally) relying on file/ paths working.

@Isvane Isvane changed the title fix(services): handle trailing slash fix(services)!: reject trailing slash for file paths May 14, 2026
Copy link
Copy Markdown
Member

@jlizen jlizen left a comment

Choose a reason for hiding this comment

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

Thanks for the contribution! Implementation could use a bit of tweaking, see comments.

minor: It might be worth applying similar validation to ServeVariant::SingleFile as well, no? Probably more confusing to have handling be inconsistent?

// Might already at this point know a redirect or not found result should be
// returned which corresponds to a Some(output). Otherwise the path might be
// modified and proceed to the open file/metadata future.
let was_directory = is_dir(&path_to_file).await;
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This will result in a redundant syscall since we anyway call is_dir inside maybe_redirect_or_append_path.

We could instead just check for the trailing slash AFTER that call, since if it doesn't have an early return, we know that we are dealing with file rather than a directory (perhaps because index.html was appended)

ServeVariant::Directory {
append_index_html_on_directories,
} => {
// Might already at this point know a redirect or not found result should be
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I would probably keep this comment, it is largely accurate still? Instead of we moved the slash check until after per this comment we would want a new comment explaining, "at this point we know we are are dealing with a file, so make sure it doesn't have a trailing slash"

negotiated_encodings: Vec<(Encoding, QValue)>,
range_header: Option<String>,
buf_chunk_size: usize,
trailing_slash: bool,
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I would probably just check the path directly inline where we need it rather than threading throughout.

}

#[tokio::test]
async fn not_found_when_file_requested_with_trailing_slash() {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Can we also have a check where there is a trailing slash + file where there is a fallback configured?

@Isvane
Copy link
Copy Markdown
Author

Isvane commented May 19, 2026

Thanks for the contribution! Implementation could use a bit of tweaking, see comments.

minor: It might be worth applying similar validation to ServeVariant::SingleFile as well, no? Probably more confusing to have handling be inconsistent?

Hi @jlizen,

Thanks so much for the review. I've updated the PR with your feedback.

I took out the redundant is_dir check and moved the trailing slash logic to happen inline right after maybe_redirect_or_append_path. I also made sure to leave the original comment alone and added a new one like you suggested.

I also did the suggestion for SingleFile. Since this is my first PR ever, I mostly just copied and pasted the existing test structures to write the two new tests (one for the fallback behavior and one for the single file trailing slash check). Hopefully everything looks okay.

I really appreciate you taking the time for the review and the helpful guidance. I learned a lot messing around with this. Let me know if there's anything else I should tweak.

@Isvane Isvane requested a review from jlizen May 19, 2026 16:02
@jplatte
Copy link
Copy Markdown
Member

jplatte commented May 19, 2026

@jlizen are you sure about SingleFile? Isn't that (only) used for ServeFile, where we (should) just ignore the request path entirely?

@jlizen
Copy link
Copy Markdown
Member

jlizen commented May 21, 2026

@jlizen are you sure about SingleFile? Isn't that (only) used for ServeFile, where we (should) just ignore the request path entirely?

Yeah, after thinking more with it, I agree that this check doesn't belong with ServeFile.

My knee-jerk previously was, "well, it is intrinsically confusing to serve a file from a directory-like URI". But, it is literally the contract of ServeFile to serve a given file regardless of path, which is already off the rail for "URI intuition", so I think the concern is misplaced.

Some(path_to_file)
}
ServeVariant::SingleFile { mime: _ } => Some(base_path.to_path_buf()),
ServeVariant::SingleFile { mime: _ } => {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Sorry for the churn on this, but let's rip out the SingleFile check as discussed here.

// At this point we know we are dealing with a file mapping.
// A trailing slash is only valid if it's the root path "/" fetching an index file, otherwise it's invalid.
let uri_path = req.uri().path();
if uri_path.ends_with('/') && uri_path != "/" {
Copy link
Copy Markdown
Member

@jlizen jlizen May 21, 2026

Choose a reason for hiding this comment

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

This introduces a bug where /foo/ with append_index_html_on_directories: true will now 404 out.

The check should instead be something like:

            if uri_path.ends_with('/') && !path_to_file.ends_with("index.html") { ...

Another option would be to return an enum from maybe_redirect_or_append_path:

  enum PathResolution {
      EarlyOutput(OpenFileOutput),
      AppendedIndexHtml,  // was a directory, index.html appended
      NotADirectory,      // not a directory, path unchanged
  }

I have no strong preference for one versus the other, dealer's choice. The enum is cleaner in principal but probably a bit overengineered compared to the one-liner.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Also, let's definitely have a test case with /foo/ that appends index.html and makes sure it is still served properly.

Let's throw in a test against / that appends index.html while we're at it.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants