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
+}