@marcel91200 / caddy Mirror · read-only / commits / 0125ae39
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, 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 +}
modules/caddyhttp/fileserver/testdata/modtime.txt