@marcel91200 / caddy Mirror · read-only / commits / cce035d7
Fork 0

Fast and extensible multi-platform HTTP/1-2-3 web server with automatic HTTPS

Mirrored from https://github.com/Slazee/caddy.git

Synced Nov 11, 01:00 UTC

fileserver: handle empty files in size try policies (#8322)

Use the chosen file info, not a zero size, to tell whether a candidate was chosen, so smallest_size keeps an empty file and largest_size can choose one.

D'Ali Ben Macha committed Nov 3, 2026 at 07:59 UTC cce035d7ad41dd8a20892b305c71a406fbd3602c
2 files changed +74 -2
modules/caddyhttp/fileserver/matcher.go
+2 -2
@@ -500,7 +500,7 @@ func (m MatchFile) selectFile(r *http.Request) (bool, error) {
500 candidates := makeCandidates(pattern)
501 for _, c := range candidates {
502 info, err := fs.Stat(fileSystem, c.fullpath)
503 - if err == nil && info.Size() > largestSize {
503 + if err == nil && (largestInfo == nil || info.Size() > largestSize) {
504 largestSize = info.Size()
505 largest = c
506 largestInfo = info
@@ -521,7 +521,7 @@ func (m MatchFile) selectFile(r *http.Request) (bool, error) {
521 candidates := makeCandidates(pattern)
522 for _, c := range candidates {
523 info, err := fs.Stat(fileSystem, c.fullpath)
524 - if err == nil && (smallestSize == 0 || info.Size() < smallestSize) {
524 + if err == nil && (smallestInfo == nil || info.Size() < smallestSize) {
525 smallestSize = info.Size()
526 smallest = c
527 smallestInfo = info
modules/caddyhttp/fileserver/matcher_test.go
+72
@@ -786,3 +786,75 @@ func TestLiteralGlobNameMatchesOnlyAsFallback(t *testing.T) {
786 }
787 }
788 }
789 +
790 +// An empty file is a valid candidate for the size policies: smallest_size
791 +// must keep it once chosen, and largest_size must be able to choose it.
792 +func TestSizePoliciesWithEmptyFiles(t *testing.T) {
793 + root := t.TempDir()
794 + for name, content := range map[string]string{
795 + "empty.txt": "",
796 + "small.txt": "x",
797 + "big.txt": "xxxxx",
798 + } {
799 + if err := os.WriteFile(filepath.Join(root, name), []byte(content), 0o644); err != nil {
800 + t.Fatal(err)
801 + }
802 + }
803 +
804 + for _, tc := range []struct {
805 + name string
806 + tryFiles []string
807 + policy string
808 + expectedPath string
809 + }{
810 + {
811 + name: "smallest_size keeps an empty file listed first",
812 + tryFiles: []string{"empty.txt", "small.txt", "big.txt"},
813 + policy: tryPolicySmallestSize,
814 + expectedPath: "/empty.txt",
815 + },
816 + {
817 + name: "smallest_size chooses an empty file listed last",
818 + tryFiles: []string{"big.txt", "small.txt", "empty.txt"},
819 + policy: tryPolicySmallestSize,
820 + expectedPath: "/empty.txt",
821 + },
822 + {
823 + name: "largest_size chooses the only file even if empty",
824 + tryFiles: []string{"missing.txt", "empty.txt"},
825 + policy: tryPolicyLargestSize,
826 + expectedPath: "/empty.txt",
827 + },
828 + {
829 + name: "largest_size prefers a larger file after an empty one",
830 + tryFiles: []string{"empty.txt", "big.txt", "small.txt"},
831 + policy: tryPolicyLargestSize,
832 + expectedPath: "/big.txt",
833 + },
834 + } {
835 + t.Run(tc.name, func(t *testing.T) {
836 + m := &MatchFile{
837 + fsmap: &filesystems.FileSystemMap{},
838 + Root: root,
839 + TryFiles: tc.tryFiles,
840 + TryPolicy: tc.policy,
841 + }
842 +
843 + req := &http.Request{URL: &url.URL{Path: "/"}}
844 + repl := caddyhttp.NewTestReplacer(req)
845 +
846 + result, err := m.MatchWithError(req)
847 + if err != nil {
848 + t.Fatalf("unexpected error: %v", err)
849 + }
850 + if !result {
851 + t.Fatal("expected a match")
852 + }
853 +
854 + rel, _ := repl.Get("http.matchers.file.relative")
855 + if rel != tc.expectedPath {
856 + t.Errorf("actual path: %v, expected: %v", rel, tc.expectedPath)
857 + }
858 + })
859 + }
860 +}