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, 11:00 UTC
caddyhttp: omit Last-Modified for unusable mod times (#7740)
See #5548 and #7730
Brett Bethke committed
May 20, 2026 at 01:19 UTC
0125ae39cccfdf9b6fdfb16d5a59f3ad37a2caf6
3 files changed
+78
-3
modules/caddyhttp/fileserver/staticfiles.go
+22
-3
@@ -29,6 +29,7 @@ import (
29
"runtime"
30
"strconv"
31
"strings"
32
+ "time"
33
34
"go.uber.org/zap"
35
"go.uber.org/zap/zapcore"
@@ -579,7 +580,17 @@ func (fsrv *FileServer) ServeHTTP(w http.ResponseWriter, r *http.Request, next c
580
// that errors generated by ServeContent are written immediately
581
// to the response, so we cannot handle them (but errors there
582
// are rare)
582
- http.ServeContent(w, r, info.Name(), info.ModTime(), file.(io.ReadSeeker))
583
+ //
584
+ // There are a few file modification times that aren't useful
585
+ // to send in Last-Modified headers, but the golang http library only
586
+ // omits Last-Modified headers for the Unix epoch time. So, force
587
+ // the modification time to the epoch time if it's not useful.
588
+ zeroTime := time.Time{}
589
+ modTime := info.ModTime()
590
+ if !usefulModTime(modTime) {
591
+ modTime = zeroTime
592
+ }
593
+ http.ServeContent(w, r, info.Name(), modTime, file.(io.ReadSeeker))
594
595
return nil
596
}
@@ -726,6 +737,14 @@ func (fsrv *FileServer) notFound(w http.ResponseWriter, r *http.Request, next ca
737
return caddyhttp.Error(http.StatusNotFound, nil)
738
}
739
740
+// Indicates whether a file's modification time is useful for validator
741
+// generation purposes (i.e. inclusion in ETag and Last-Modified headers).
742
+// See issues #5548 and #7730.
743
+func usefulModTime(modTime time.Time) bool {
744
+ mtimeunix := modTime.Unix()
745
+ return mtimeunix != 0 && mtimeunix != 1
746
+}
747
+
748
// calculateEtag computes an entity tag using a strong validator
749
// without consuming the contents of the file. It requires the
750
// file info contain the correct size and modification time.
@@ -743,8 +762,8 @@ func (fsrv *FileServer) notFound(w http.ResponseWriter, r *http.Request, next ca
762
// which we consider precise enough to qualify as a strong validator.
763
func calculateEtag(d os.FileInfo) string {
764
mtime := d.ModTime()
746
- if mtimeUnix := mtime.Unix(); mtimeUnix == 0 || mtimeUnix == 1 {
747
- return "" // not useful anyway; see issue #5548
765
+ if !usefulModTime(mtime) {
766
+ return ""
767
}
768
var sb strings.Builder
769
sb.WriteRune('"')
modules/caddyhttp/fileserver/staticfiles_test.go
+56
@@ -15,10 +15,17 @@
15
package fileserver
16
17
import (
18
+ "context"
19
+ "net/http"
20
+ "net/http/httptest"
21
+ "os"
22
"path/filepath"
23
"runtime"
24
"strings"
25
"testing"
26
+ "time"
27
+
28
+ "github.com/caddyserver/caddy/v2"
29
)
30
31
func TestFileHidden(t *testing.T) {
@@ -128,3 +135,52 @@ func TestFileHidden(t *testing.T) {
135
}
136
}
137
}
138
+
139
+// Check to make sure that we don't serve ETag and Last-Modified headers
140
+// for files with invalid modification times
141
+func TestModTimeHeaders(t *testing.T) {
142
+ check_validator_headers(time.Now(), true, t)
143
+ check_validator_headers(time.Unix(0, 0), false, t)
144
+ check_validator_headers(time.Unix(1, 0), false, t)
145
+ check_validator_headers(time.Unix(2, 0), true, t)
146
+}
147
+
148
+func check_validator_headers(modTime time.Time, expect_headers bool, t *testing.T) {
149
+ f := false
150
+ fsrv := FileServer{
151
+ Root: "./testdata",
152
+ CanonicalURIs: &f,
153
+ }
154
+ w := httptest.NewRecorder()
155
+ r, err := http.NewRequest("GET", "/modtime.txt", nil)
156
+ if err != nil {
157
+ t.Fatal(err)
158
+ }
159
+ repl := caddy.NewReplacer()
160
+ ctx := context.WithValue(r.Context(), caddy.ReplacerCtxKey, repl)
161
+ r = r.WithContext(ctx)
162
+
163
+ ctx2, _ := caddy.NewContext(caddy.Context{Context: context.Background()}) // module will be nil by default
164
+ fsrv.Provision(ctx2)
165
+
166
+ path := "testdata/modtime.txt"
167
+ os.Chtimes(path, modTime, modTime)
168
+
169
+ fsrv.ServeHTTP(w, r, nil)
170
+
171
+ if expect_headers {
172
+ if w.Header().Get("ETag") == "" {
173
+ t.Errorf("Didn't get ETag header for file with valid mod time %s", modTime)
174
+ }
175
+ if w.Header().Get("Last-Modified") == "" {
176
+ t.Errorf("Didn't get Last-Modified header for file with valid mod time %s", modTime)
177
+ }
178
+ } else {
179
+ if w.Header().Get("ETag") != "" {
180
+ t.Errorf("Got ETag header for file with invalid mod time %s", modTime)
181
+ }
182
+ if w.Header().Get("Last-Modified") != "" {
183
+ t.Errorf("Got Last-Modified header for file with invalid mod time %s", modTime)
184
+ }
185
+ }
186
+}