Skip to content

Commit 3996518

Browse files
authored
Refactor cache-control (#33861)
And fix #21391
1 parent 91610a9 commit 3996518

File tree

15 files changed

+95
-65
lines changed

15 files changed

+95
-65
lines changed

modules/httpcache/httpcache.go

Lines changed: 37 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -4,40 +4,60 @@
44
package httpcache
55

66
import (
7-
"io"
7+
"fmt"
88
"net/http"
99
"strconv"
1010
"strings"
1111
"time"
1212

1313
"code.gitea.io/gitea/modules/setting"
14+
"code.gitea.io/gitea/modules/util"
1415
)
1516

17+
type CacheControlOptions struct {
18+
IsPublic bool
19+
MaxAge time.Duration
20+
NoTransform bool
21+
}
22+
1623
// SetCacheControlInHeader sets suitable cache-control headers in the response
17-
func SetCacheControlInHeader(h http.Header, maxAge time.Duration, additionalDirectives ...string) {
18-
directives := make([]string, 0, 2+len(additionalDirectives))
24+
func SetCacheControlInHeader(h http.Header, opts *CacheControlOptions) {
25+
directives := make([]string, 0, 4)
1926

2027
// "max-age=0 + must-revalidate" (aka "no-cache") is preferred instead of "no-store"
2128
// because browsers may restore some input fields after navigate-back / reload a page.
29+
publicPrivate := util.Iif(opts.IsPublic, "public", "private")
2230
if setting.IsProd {
23-
if maxAge == 0 {
31+
if opts.MaxAge == 0 {
2432
directives = append(directives, "max-age=0", "private", "must-revalidate")
2533
} else {
26-
directives = append(directives, "private", "max-age="+strconv.Itoa(int(maxAge.Seconds())))
34+
directives = append(directives, publicPrivate, "max-age="+strconv.Itoa(int(opts.MaxAge.Seconds())))
2735
}
2836
} else {
29-
directives = append(directives, "max-age=0", "private", "must-revalidate")
37+
// use dev-related controls, and remind users they are using non-prod setting.
38+
directives = append(directives, "max-age=0", publicPrivate, "must-revalidate")
39+
h.Set("X-Gitea-Debug", fmt.Sprintf("RUN_MODE=%v, MaxAge=%s", setting.RunMode, opts.MaxAge))
40+
}
3041

31-
// to remind users they are using non-prod setting.
32-
h.Set("X-Gitea-Debug", "RUN_MODE="+setting.RunMode)
42+
if opts.NoTransform {
43+
directives = append(directives, "no-transform")
3344
}
45+
h.Set("Cache-Control", strings.Join(directives, ", "))
46+
}
3447

35-
h.Set("Cache-Control", strings.Join(append(directives, additionalDirectives...), ", "))
48+
func CacheControlForPublicStatic() *CacheControlOptions {
49+
return &CacheControlOptions{
50+
IsPublic: true,
51+
MaxAge: setting.StaticCacheTime,
52+
NoTransform: true,
53+
}
3654
}
3755

38-
func ServeContentWithCacheControl(w http.ResponseWriter, req *http.Request, name string, modTime time.Time, content io.ReadSeeker) {
39-
SetCacheControlInHeader(w.Header(), setting.StaticCacheTime)
40-
http.ServeContent(w, req, name, modTime, content)
56+
func CacheControlForPrivateStatic() *CacheControlOptions {
57+
return &CacheControlOptions{
58+
MaxAge: setting.StaticCacheTime,
59+
NoTransform: true,
60+
}
4161
}
4262

4363
// HandleGenericETagCache handles ETag-based caching for a HTTP request.
@@ -50,7 +70,8 @@ func HandleGenericETagCache(req *http.Request, w http.ResponseWriter, etag strin
5070
return true
5171
}
5272
}
53-
SetCacheControlInHeader(w.Header(), setting.StaticCacheTime)
73+
// not sure whether it is a public content, so just use "private" (old behavior)
74+
SetCacheControlInHeader(w.Header(), CacheControlForPrivateStatic())
5475
return false
5576
}
5677

@@ -95,6 +116,8 @@ func HandleGenericETagTimeCache(req *http.Request, w http.ResponseWriter, etag s
95116
}
96117
}
97118
}
98-
SetCacheControlInHeader(w.Header(), setting.StaticCacheTime)
119+
120+
// not sure whether it is a public content, so just use "private" (old behavior)
121+
SetCacheControlInHeader(w.Header(), CacheControlForPrivateStatic())
99122
return false
100123
}

modules/httplib/serve.go

Lines changed: 14 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ type ServeHeaderOptions struct {
3333
ContentLength *int64
3434
Disposition string // defaults to "attachment"
3535
Filename string
36+
CacheIsPublic bool
3637
CacheDuration time.Duration // defaults to 5 minutes
3738
LastModified time.Time
3839
}
@@ -72,11 +73,11 @@ func ServeSetHeaders(w http.ResponseWriter, opts *ServeHeaderOptions) {
7273
header.Set("Access-Control-Expose-Headers", "Content-Disposition")
7374
}
7475

75-
duration := opts.CacheDuration
76-
if duration == 0 {
77-
duration = 5 * time.Minute
78-
}
79-
httpcache.SetCacheControlInHeader(header, duration)
76+
httpcache.SetCacheControlInHeader(header, &httpcache.CacheControlOptions{
77+
IsPublic: opts.CacheIsPublic,
78+
MaxAge: opts.CacheDuration,
79+
NoTransform: true,
80+
})
8081

8182
if !opts.LastModified.IsZero() {
8283
// http.TimeFormat required a UTC time, refer to https://pkg.go.dev/net/http#TimeFormat
@@ -85,19 +86,15 @@ func ServeSetHeaders(w http.ResponseWriter, opts *ServeHeaderOptions) {
8586
}
8687

8788
// ServeData download file from io.Reader
88-
func setServeHeadersByFile(r *http.Request, w http.ResponseWriter, filePath string, mineBuf []byte) {
89+
func setServeHeadersByFile(r *http.Request, w http.ResponseWriter, mineBuf []byte, opts *ServeHeaderOptions) {
8990
// do not set "Content-Length", because the length could only be set by callers, and it needs to support range requests
90-
opts := &ServeHeaderOptions{
91-
Filename: path.Base(filePath),
92-
}
93-
9491
sniffedType := typesniffer.DetectContentType(mineBuf)
9592

9693
// the "render" parameter came from year 2016: 638dd24c, it doesn't have clear meaning, so I think it could be removed later
9794
isPlain := sniffedType.IsText() || r.FormValue("render") != ""
9895

9996
if setting.MimeTypeMap.Enabled {
100-
fileExtension := strings.ToLower(filepath.Ext(filePath))
97+
fileExtension := strings.ToLower(filepath.Ext(opts.Filename))
10198
opts.ContentType = setting.MimeTypeMap.Map[fileExtension]
10299
}
103100

@@ -114,7 +111,7 @@ func setServeHeadersByFile(r *http.Request, w http.ResponseWriter, filePath stri
114111
if isPlain {
115112
charset, err := charsetModule.DetectEncoding(mineBuf)
116113
if err != nil {
117-
log.Error("Detect raw file %s charset failed: %v, using by default utf-8", filePath, err)
114+
log.Error("Detect raw file %s charset failed: %v, using by default utf-8", opts.Filename, err)
118115
charset = "utf-8"
119116
}
120117
opts.ContentTypeCharset = strings.ToLower(charset)
@@ -142,7 +139,7 @@ func setServeHeadersByFile(r *http.Request, w http.ResponseWriter, filePath stri
142139

143140
const mimeDetectionBufferLen = 1024
144141

145-
func ServeContentByReader(r *http.Request, w http.ResponseWriter, filePath string, size int64, reader io.Reader) {
142+
func ServeContentByReader(r *http.Request, w http.ResponseWriter, size int64, reader io.Reader, opts *ServeHeaderOptions) {
146143
buf := make([]byte, mimeDetectionBufferLen)
147144
n, err := util.ReadAtMost(reader, buf)
148145
if err != nil {
@@ -152,7 +149,7 @@ func ServeContentByReader(r *http.Request, w http.ResponseWriter, filePath strin
152149
if n >= 0 {
153150
buf = buf[:n]
154151
}
155-
setServeHeadersByFile(r, w, filePath, buf)
152+
setServeHeadersByFile(r, w, buf, opts)
156153

157154
// reset the reader to the beginning
158155
reader = io.MultiReader(bytes.NewReader(buf), reader)
@@ -215,7 +212,7 @@ func ServeContentByReader(r *http.Request, w http.ResponseWriter, filePath strin
215212
_, _ = io.CopyN(w, reader, partialLength) // just like http.ServeContent, not necessary to handle the error
216213
}
217214

218-
func ServeContentByReadSeeker(r *http.Request, w http.ResponseWriter, filePath string, modTime *time.Time, reader io.ReadSeeker) {
215+
func ServeContentByReadSeeker(r *http.Request, w http.ResponseWriter, modTime *time.Time, reader io.ReadSeeker, opts *ServeHeaderOptions) {
219216
buf := make([]byte, mimeDetectionBufferLen)
220217
n, err := util.ReadAtMost(reader, buf)
221218
if err != nil {
@@ -229,9 +226,9 @@ func ServeContentByReadSeeker(r *http.Request, w http.ResponseWriter, filePath s
229226
if n >= 0 {
230227
buf = buf[:n]
231228
}
232-
setServeHeadersByFile(r, w, filePath, buf)
229+
setServeHeadersByFile(r, w, buf, opts)
233230
if modTime == nil {
234231
modTime = &time.Time{}
235232
}
236-
http.ServeContent(w, r, path.Base(filePath), *modTime, reader)
233+
http.ServeContent(w, r, opts.Filename, *modTime, reader)
237234
}

modules/httplib/serve_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ func TestServeContentByReader(t *testing.T) {
2727
}
2828
reader := strings.NewReader(data)
2929
w := httptest.NewRecorder()
30-
ServeContentByReader(r, w, "test", int64(len(data)), reader)
30+
ServeContentByReader(r, w, int64(len(data)), reader, &ServeHeaderOptions{})
3131
assert.Equal(t, expectedStatusCode, w.Code)
3232
if expectedStatusCode == http.StatusPartialContent || expectedStatusCode == http.StatusOK {
3333
assert.Equal(t, fmt.Sprint(len(expectedContent)), w.Header().Get("Content-Length"))
@@ -76,7 +76,7 @@ func TestServeContentByReadSeeker(t *testing.T) {
7676
defer seekReader.Close()
7777

7878
w := httptest.NewRecorder()
79-
ServeContentByReadSeeker(r, w, "test", nil, seekReader)
79+
ServeContentByReadSeeker(r, w, nil, seekReader, &ServeHeaderOptions{})
8080
assert.Equal(t, expectedStatusCode, w.Code)
8181
if expectedStatusCode == http.StatusPartialContent || expectedStatusCode == http.StatusOK {
8282
assert.Equal(t, fmt.Sprint(len(expectedContent)), w.Header().Get("Content-Length"))

modules/public/public.go

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -86,17 +86,17 @@ func handleRequest(w http.ResponseWriter, req *http.Request, fs http.FileSystem,
8686
return
8787
}
8888

89-
serveContent(w, req, fi, fi.ModTime(), f)
89+
servePublicAsset(w, req, fi, fi.ModTime(), f)
9090
}
9191

9292
type GzipBytesProvider interface {
9393
GzipBytes() []byte
9494
}
9595

96-
// serveContent serve http content
97-
func serveContent(w http.ResponseWriter, req *http.Request, fi os.FileInfo, modtime time.Time, content io.ReadSeeker) {
96+
// servePublicAsset serve http content
97+
func servePublicAsset(w http.ResponseWriter, req *http.Request, fi os.FileInfo, modtime time.Time, content io.ReadSeeker) {
9898
setWellKnownContentType(w, fi.Name())
99-
99+
httpcache.SetCacheControlInHeader(w.Header(), httpcache.CacheControlForPublicStatic())
100100
encodings := parseAcceptEncoding(req.Header.Get("Accept-Encoding"))
101101
if encodings.Contains("gzip") {
102102
// try to provide gzip content directly from bindata (provided by vfsgen۰CompressedFileInfo)
@@ -108,11 +108,10 @@ func serveContent(w http.ResponseWriter, req *http.Request, fi os.FileInfo, modt
108108
w.Header().Set("Content-Type", "application/octet-stream")
109109
}
110110
w.Header().Set("Content-Encoding", "gzip")
111-
httpcache.ServeContentWithCacheControl(w, req, fi.Name(), modtime, rdGzip)
111+
http.ServeContent(w, req, fi.Name(), modtime, rdGzip)
112112
return
113113
}
114114
}
115-
116-
httpcache.ServeContentWithCacheControl(w, req, fi.Name(), modtime, content)
115+
http.ServeContent(w, req, fi.Name(), modtime, content)
117116
return
118117
}

routers/api/v1/repo/file.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ func GetRawFile(ctx *context.APIContext) {
8383

8484
ctx.RespHeader().Set(giteaObjectTypeHeader, string(files_service.GetObjectTypeFromTreeEntry(entry)))
8585

86-
if err := common.ServeBlob(ctx.Base, ctx.Repo.TreePath, blob, lastModified); err != nil {
86+
if err := common.ServeBlob(ctx.Base, ctx.Repo.Repository, ctx.Repo.TreePath, blob, lastModified); err != nil {
8787
ctx.APIErrorInternal(err)
8888
}
8989
}
@@ -144,7 +144,7 @@ func GetRawFileOrLFS(ctx *context.APIContext) {
144144
}
145145

146146
// OK not cached - serve!
147-
if err := common.ServeBlob(ctx.Base, ctx.Repo.TreePath, blob, lastModified); err != nil {
147+
if err := common.ServeBlob(ctx.Base, ctx.Repo.Repository, ctx.Repo.TreePath, blob, lastModified); err != nil {
148148
ctx.APIErrorInternal(err)
149149
}
150150
return

routers/common/errpage.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ func RenderPanicErrorPage(w http.ResponseWriter, req *http.Request, err any) {
3232

3333
routing.UpdatePanicError(req.Context(), err)
3434

35-
httpcache.SetCacheControlInHeader(w.Header(), 0, "no-transform")
35+
httpcache.SetCacheControlInHeader(w.Header(), &httpcache.CacheControlOptions{NoTransform: true})
3636
w.Header().Set(`X-Frame-Options`, setting.CORSConfig.XFrameOptions)
3737

3838
tmplCtx := context.TemplateContext{}

routers/common/serve.go

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,17 +5,21 @@ package common
55

66
import (
77
"io"
8+
"path"
89
"time"
910

11+
repo_model "code.gitea.io/gitea/models/repo"
1012
"code.gitea.io/gitea/modules/git"
1113
"code.gitea.io/gitea/modules/httpcache"
1214
"code.gitea.io/gitea/modules/httplib"
1315
"code.gitea.io/gitea/modules/log"
16+
"code.gitea.io/gitea/modules/setting"
17+
"code.gitea.io/gitea/modules/structs"
1418
"code.gitea.io/gitea/services/context"
1519
)
1620

1721
// ServeBlob download a git.Blob
18-
func ServeBlob(ctx *context.Base, filePath string, blob *git.Blob, lastModified *time.Time) error {
22+
func ServeBlob(ctx *context.Base, repo *repo_model.Repository, filePath string, blob *git.Blob, lastModified *time.Time) error {
1923
if httpcache.HandleGenericETagTimeCache(ctx.Req, ctx.Resp, `"`+blob.ID.String()+`"`, lastModified) {
2024
return nil
2125
}
@@ -30,14 +34,19 @@ func ServeBlob(ctx *context.Base, filePath string, blob *git.Blob, lastModified
3034
}
3135
}()
3236

33-
httplib.ServeContentByReader(ctx.Req, ctx.Resp, filePath, blob.Size(), dataRc)
37+
_ = repo.LoadOwner(ctx)
38+
httplib.ServeContentByReader(ctx.Req, ctx.Resp, blob.Size(), dataRc, &httplib.ServeHeaderOptions{
39+
Filename: path.Base(filePath),
40+
CacheIsPublic: !repo.IsPrivate && repo.Owner != nil && repo.Owner.Visibility == structs.VisibleTypePublic,
41+
CacheDuration: setting.StaticCacheTime,
42+
})
3443
return nil
3544
}
3645

3746
func ServeContentByReader(ctx *context.Base, filePath string, size int64, reader io.Reader) {
38-
httplib.ServeContentByReader(ctx.Req, ctx.Resp, filePath, size, reader)
47+
httplib.ServeContentByReader(ctx.Req, ctx.Resp, size, reader, &httplib.ServeHeaderOptions{Filename: path.Base(filePath)})
3948
}
4049

4150
func ServeContentByReadSeeker(ctx *context.Base, filePath string, modTime *time.Time, reader io.ReadSeeker) {
42-
httplib.ServeContentByReadSeeker(ctx.Req, ctx.Resp, filePath, modTime, reader)
51+
httplib.ServeContentByReadSeeker(ctx.Req, ctx.Resp, modTime, reader, &httplib.ServeHeaderOptions{Filename: path.Base(filePath)})
4352
}

routers/web/base.go

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -19,12 +19,12 @@ import (
1919
"code.gitea.io/gitea/modules/web/routing"
2020
)
2121

22-
func storageHandler(storageSetting *setting.Storage, prefix string, objStore storage.ObjectStorage) http.HandlerFunc {
22+
func avatarStorageHandler(storageSetting *setting.Storage, prefix string, objStore storage.ObjectStorage) http.HandlerFunc {
2323
prefix = strings.Trim(prefix, "/")
24-
funcInfo := routing.GetFuncInfo(storageHandler, prefix)
24+
funcInfo := routing.GetFuncInfo(avatarStorageHandler, prefix)
2525

2626
if storageSetting.ServeDirect() {
27-
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
27+
return func(w http.ResponseWriter, req *http.Request) {
2828
if req.Method != "GET" && req.Method != "HEAD" {
2929
http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed)
3030
return
@@ -52,10 +52,10 @@ func storageHandler(storageSetting *setting.Storage, prefix string, objStore sto
5252
}
5353

5454
http.Redirect(w, req, u.String(), http.StatusTemporaryRedirect)
55-
})
55+
}
5656
}
5757

58-
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
58+
return func(w http.ResponseWriter, req *http.Request) {
5959
if req.Method != "GET" && req.Method != "HEAD" {
6060
http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed)
6161
return
@@ -93,6 +93,8 @@ func storageHandler(storageSetting *setting.Storage, prefix string, objStore sto
9393
return
9494
}
9595
defer fr.Close()
96-
httpcache.ServeContentWithCacheControl(w, req, path.Base(rPath), fi.ModTime(), fr)
97-
})
96+
97+
httpcache.SetCacheControlInHeader(w.Header(), httpcache.CacheControlForPublicStatic())
98+
http.ServeContent(w, req, path.Base(rPath), fi.ModTime(), fr)
99+
}
98100
}

routers/web/misc/misc.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ func RobotsTxt(w http.ResponseWriter, req *http.Request) {
3838
if ok, _ := util.IsExist(robotsTxt); !ok {
3939
robotsTxt = util.FilePathJoinAbs(setting.CustomPath, "robots.txt") // the legacy "robots.txt"
4040
}
41-
httpcache.SetCacheControlInHeader(w.Header(), setting.StaticCacheTime)
41+
httpcache.SetCacheControlInHeader(w.Header(), httpcache.CacheControlForPublicStatic())
4242
http.ServeFile(w, req, robotsTxt)
4343
}
4444

routers/web/repo/download.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ func ServeBlobOrLFS(ctx *context.Context, blob *git.Blob, lastModified *time.Tim
4646
log.Error("ServeBlobOrLFS: Close: %v", err)
4747
}
4848
closed = true
49-
return common.ServeBlob(ctx.Base, ctx.Repo.TreePath, blob, lastModified)
49+
return common.ServeBlob(ctx.Base, ctx.Repo.Repository, ctx.Repo.TreePath, blob, lastModified)
5050
}
5151
if httpcache.HandleGenericETagCache(ctx.Req, ctx.Resp, `"`+pointer.Oid+`"`) {
5252
return nil
@@ -78,7 +78,7 @@ func ServeBlobOrLFS(ctx *context.Context, blob *git.Blob, lastModified *time.Tim
7878
}
7979
closed = true
8080

81-
return common.ServeBlob(ctx.Base, ctx.Repo.TreePath, blob, lastModified)
81+
return common.ServeBlob(ctx.Base, ctx.Repo.Repository, ctx.Repo.TreePath, blob, lastModified)
8282
}
8383

8484
func getBlobForEntry(ctx *context.Context) (*git.Blob, *time.Time) {
@@ -114,7 +114,7 @@ func SingleDownload(ctx *context.Context) {
114114
return
115115
}
116116

117-
if err := common.ServeBlob(ctx.Base, ctx.Repo.TreePath, blob, lastModified); err != nil {
117+
if err := common.ServeBlob(ctx.Base, ctx.Repo.Repository, ctx.Repo.TreePath, blob, lastModified); err != nil {
118118
ctx.ServerError("ServeBlob", err)
119119
}
120120
}
@@ -142,7 +142,7 @@ func DownloadByID(ctx *context.Context) {
142142
}
143143
return
144144
}
145-
if err = common.ServeBlob(ctx.Base, ctx.Repo.TreePath, blob, nil); err != nil {
145+
if err = common.ServeBlob(ctx.Base, ctx.Repo.Repository, ctx.Repo.TreePath, blob, nil); err != nil {
146146
ctx.ServerError("ServeBlob", err)
147147
}
148148
}

0 commit comments

Comments
 (0)