From 3996518ed432218d7f2fd62d451ee0e29953d853 Mon Sep 17 00:00:00 2001
From: wxiaoguang <wxiaoguang@gmail.com>
Date: Thu, 13 Mar 2025 07:04:50 +0800
Subject: [PATCH] Refactor cache-control (#33861)

And fix #21391
---
 modules/httpcache/httpcache.go | 53 ++++++++++++++++++++++++----------
 modules/httplib/serve.go       | 31 +++++++++-----------
 modules/httplib/serve_test.go  |  4 +--
 modules/public/public.go       | 13 ++++-----
 routers/api/v1/repo/file.go    |  4 +--
 routers/common/errpage.go      |  2 +-
 routers/common/serve.go        | 17 ++++++++---
 routers/web/base.go            | 16 +++++-----
 routers/web/misc/misc.go       |  2 +-
 routers/web/repo/download.go   |  8 ++---
 routers/web/repo/wiki.go       |  2 +-
 routers/web/user/avatar.go     |  2 +-
 routers/web/web.go             |  4 +--
 services/context/api.go        |  2 +-
 services/context/context.go    |  2 +-
 15 files changed, 96 insertions(+), 66 deletions(-)

diff --git a/modules/httpcache/httpcache.go b/modules/httpcache/httpcache.go
index 2c9af94405..045b00d944 100644
--- a/modules/httpcache/httpcache.go
+++ b/modules/httpcache/httpcache.go
@@ -4,40 +4,60 @@
 package httpcache
 
 import (
-	"io"
+	"fmt"
 	"net/http"
 	"strconv"
 	"strings"
 	"time"
 
 	"code.gitea.io/gitea/modules/setting"
+	"code.gitea.io/gitea/modules/util"
 )
 
+type CacheControlOptions struct {
+	IsPublic    bool
+	MaxAge      time.Duration
+	NoTransform bool
+}
+
 // SetCacheControlInHeader sets suitable cache-control headers in the response
-func SetCacheControlInHeader(h http.Header, maxAge time.Duration, additionalDirectives ...string) {
-	directives := make([]string, 0, 2+len(additionalDirectives))
+func SetCacheControlInHeader(h http.Header, opts *CacheControlOptions) {
+	directives := make([]string, 0, 4)
 
 	// "max-age=0 + must-revalidate" (aka "no-cache") is preferred instead of "no-store"
 	// because browsers may restore some input fields after navigate-back / reload a page.
+	publicPrivate := util.Iif(opts.IsPublic, "public", "private")
 	if setting.IsProd {
-		if maxAge == 0 {
+		if opts.MaxAge == 0 {
 			directives = append(directives, "max-age=0", "private", "must-revalidate")
 		} else {
-			directives = append(directives, "private", "max-age="+strconv.Itoa(int(maxAge.Seconds())))
+			directives = append(directives, publicPrivate, "max-age="+strconv.Itoa(int(opts.MaxAge.Seconds())))
 		}
 	} else {
-		directives = append(directives, "max-age=0", "private", "must-revalidate")
-
-		// to remind users they are using non-prod setting.
-		h.Set("X-Gitea-Debug", "RUN_MODE="+setting.RunMode)
+		// use dev-related controls, and remind users they are using non-prod setting.
+		directives = append(directives, "max-age=0", publicPrivate, "must-revalidate")
+		h.Set("X-Gitea-Debug", fmt.Sprintf("RUN_MODE=%v, MaxAge=%s", setting.RunMode, opts.MaxAge))
 	}
 
-	h.Set("Cache-Control", strings.Join(append(directives, additionalDirectives...), ", "))
+	if opts.NoTransform {
+		directives = append(directives, "no-transform")
+	}
+	h.Set("Cache-Control", strings.Join(directives, ", "))
 }
 
-func ServeContentWithCacheControl(w http.ResponseWriter, req *http.Request, name string, modTime time.Time, content io.ReadSeeker) {
-	SetCacheControlInHeader(w.Header(), setting.StaticCacheTime)
-	http.ServeContent(w, req, name, modTime, content)
+func CacheControlForPublicStatic() *CacheControlOptions {
+	return &CacheControlOptions{
+		IsPublic:    true,
+		MaxAge:      setting.StaticCacheTime,
+		NoTransform: true,
+	}
+}
+
+func CacheControlForPrivateStatic() *CacheControlOptions {
+	return &CacheControlOptions{
+		MaxAge:      setting.StaticCacheTime,
+		NoTransform: true,
+	}
 }
 
 // HandleGenericETagCache handles ETag-based caching for a HTTP request.
@@ -50,7 +70,8 @@ func HandleGenericETagCache(req *http.Request, w http.ResponseWriter, etag strin
 			return true
 		}
 	}
-	SetCacheControlInHeader(w.Header(), setting.StaticCacheTime)
+	// not sure whether it is a public content, so just use "private" (old behavior)
+	SetCacheControlInHeader(w.Header(), CacheControlForPrivateStatic())
 	return false
 }
 
@@ -95,6 +116,8 @@ func HandleGenericETagTimeCache(req *http.Request, w http.ResponseWriter, etag s
 			}
 		}
 	}
-	SetCacheControlInHeader(w.Header(), setting.StaticCacheTime)
+
+	// not sure whether it is a public content, so just use "private" (old behavior)
+	SetCacheControlInHeader(w.Header(), CacheControlForPrivateStatic())
 	return false
 }
diff --git a/modules/httplib/serve.go b/modules/httplib/serve.go
index 8fb667876e..7c1edf432d 100644
--- a/modules/httplib/serve.go
+++ b/modules/httplib/serve.go
@@ -33,6 +33,7 @@ type ServeHeaderOptions struct {
 	ContentLength      *int64
 	Disposition        string // defaults to "attachment"
 	Filename           string
+	CacheIsPublic      bool
 	CacheDuration      time.Duration // defaults to 5 minutes
 	LastModified       time.Time
 }
@@ -72,11 +73,11 @@ func ServeSetHeaders(w http.ResponseWriter, opts *ServeHeaderOptions) {
 		header.Set("Access-Control-Expose-Headers", "Content-Disposition")
 	}
 
-	duration := opts.CacheDuration
-	if duration == 0 {
-		duration = 5 * time.Minute
-	}
-	httpcache.SetCacheControlInHeader(header, duration)
+	httpcache.SetCacheControlInHeader(header, &httpcache.CacheControlOptions{
+		IsPublic:    opts.CacheIsPublic,
+		MaxAge:      opts.CacheDuration,
+		NoTransform: true,
+	})
 
 	if !opts.LastModified.IsZero() {
 		// 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) {
 }
 
 // ServeData download file from io.Reader
-func setServeHeadersByFile(r *http.Request, w http.ResponseWriter, filePath string, mineBuf []byte) {
+func setServeHeadersByFile(r *http.Request, w http.ResponseWriter, mineBuf []byte, opts *ServeHeaderOptions) {
 	// do not set "Content-Length", because the length could only be set by callers, and it needs to support range requests
-	opts := &ServeHeaderOptions{
-		Filename: path.Base(filePath),
-	}
-
 	sniffedType := typesniffer.DetectContentType(mineBuf)
 
 	// the "render" parameter came from year 2016: 638dd24c, it doesn't have clear meaning, so I think it could be removed later
 	isPlain := sniffedType.IsText() || r.FormValue("render") != ""
 
 	if setting.MimeTypeMap.Enabled {
-		fileExtension := strings.ToLower(filepath.Ext(filePath))
+		fileExtension := strings.ToLower(filepath.Ext(opts.Filename))
 		opts.ContentType = setting.MimeTypeMap.Map[fileExtension]
 	}
 
@@ -114,7 +111,7 @@ func setServeHeadersByFile(r *http.Request, w http.ResponseWriter, filePath stri
 	if isPlain {
 		charset, err := charsetModule.DetectEncoding(mineBuf)
 		if err != nil {
-			log.Error("Detect raw file %s charset failed: %v, using by default utf-8", filePath, err)
+			log.Error("Detect raw file %s charset failed: %v, using by default utf-8", opts.Filename, err)
 			charset = "utf-8"
 		}
 		opts.ContentTypeCharset = strings.ToLower(charset)
@@ -142,7 +139,7 @@ func setServeHeadersByFile(r *http.Request, w http.ResponseWriter, filePath stri
 
 const mimeDetectionBufferLen = 1024
 
-func ServeContentByReader(r *http.Request, w http.ResponseWriter, filePath string, size int64, reader io.Reader) {
+func ServeContentByReader(r *http.Request, w http.ResponseWriter, size int64, reader io.Reader, opts *ServeHeaderOptions) {
 	buf := make([]byte, mimeDetectionBufferLen)
 	n, err := util.ReadAtMost(reader, buf)
 	if err != nil {
@@ -152,7 +149,7 @@ func ServeContentByReader(r *http.Request, w http.ResponseWriter, filePath strin
 	if n >= 0 {
 		buf = buf[:n]
 	}
-	setServeHeadersByFile(r, w, filePath, buf)
+	setServeHeadersByFile(r, w, buf, opts)
 
 	// reset the reader to the beginning
 	reader = io.MultiReader(bytes.NewReader(buf), reader)
@@ -215,7 +212,7 @@ func ServeContentByReader(r *http.Request, w http.ResponseWriter, filePath strin
 	_, _ = io.CopyN(w, reader, partialLength) // just like http.ServeContent, not necessary to handle the error
 }
 
-func ServeContentByReadSeeker(r *http.Request, w http.ResponseWriter, filePath string, modTime *time.Time, reader io.ReadSeeker) {
+func ServeContentByReadSeeker(r *http.Request, w http.ResponseWriter, modTime *time.Time, reader io.ReadSeeker, opts *ServeHeaderOptions) {
 	buf := make([]byte, mimeDetectionBufferLen)
 	n, err := util.ReadAtMost(reader, buf)
 	if err != nil {
@@ -229,9 +226,9 @@ func ServeContentByReadSeeker(r *http.Request, w http.ResponseWriter, filePath s
 	if n >= 0 {
 		buf = buf[:n]
 	}
-	setServeHeadersByFile(r, w, filePath, buf)
+	setServeHeadersByFile(r, w, buf, opts)
 	if modTime == nil {
 		modTime = &time.Time{}
 	}
-	http.ServeContent(w, r, path.Base(filePath), *modTime, reader)
+	http.ServeContent(w, r, opts.Filename, *modTime, reader)
 }
diff --git a/modules/httplib/serve_test.go b/modules/httplib/serve_test.go
index e53f38b697..06c95bc594 100644
--- a/modules/httplib/serve_test.go
+++ b/modules/httplib/serve_test.go
@@ -27,7 +27,7 @@ func TestServeContentByReader(t *testing.T) {
 		}
 		reader := strings.NewReader(data)
 		w := httptest.NewRecorder()
-		ServeContentByReader(r, w, "test", int64(len(data)), reader)
+		ServeContentByReader(r, w, int64(len(data)), reader, &ServeHeaderOptions{})
 		assert.Equal(t, expectedStatusCode, w.Code)
 		if expectedStatusCode == http.StatusPartialContent || expectedStatusCode == http.StatusOK {
 			assert.Equal(t, fmt.Sprint(len(expectedContent)), w.Header().Get("Content-Length"))
@@ -76,7 +76,7 @@ func TestServeContentByReadSeeker(t *testing.T) {
 		defer seekReader.Close()
 
 		w := httptest.NewRecorder()
-		ServeContentByReadSeeker(r, w, "test", nil, seekReader)
+		ServeContentByReadSeeker(r, w, nil, seekReader, &ServeHeaderOptions{})
 		assert.Equal(t, expectedStatusCode, w.Code)
 		if expectedStatusCode == http.StatusPartialContent || expectedStatusCode == http.StatusOK {
 			assert.Equal(t, fmt.Sprint(len(expectedContent)), w.Header().Get("Content-Length"))
diff --git a/modules/public/public.go b/modules/public/public.go
index abc6b46158..7f8ce29056 100644
--- a/modules/public/public.go
+++ b/modules/public/public.go
@@ -86,17 +86,17 @@ func handleRequest(w http.ResponseWriter, req *http.Request, fs http.FileSystem,
 		return
 	}
 
-	serveContent(w, req, fi, fi.ModTime(), f)
+	servePublicAsset(w, req, fi, fi.ModTime(), f)
 }
 
 type GzipBytesProvider interface {
 	GzipBytes() []byte
 }
 
-// serveContent serve http content
-func serveContent(w http.ResponseWriter, req *http.Request, fi os.FileInfo, modtime time.Time, content io.ReadSeeker) {
+// servePublicAsset serve http content
+func servePublicAsset(w http.ResponseWriter, req *http.Request, fi os.FileInfo, modtime time.Time, content io.ReadSeeker) {
 	setWellKnownContentType(w, fi.Name())
-
+	httpcache.SetCacheControlInHeader(w.Header(), httpcache.CacheControlForPublicStatic())
 	encodings := parseAcceptEncoding(req.Header.Get("Accept-Encoding"))
 	if encodings.Contains("gzip") {
 		// 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
 				w.Header().Set("Content-Type", "application/octet-stream")
 			}
 			w.Header().Set("Content-Encoding", "gzip")
-			httpcache.ServeContentWithCacheControl(w, req, fi.Name(), modtime, rdGzip)
+			http.ServeContent(w, req, fi.Name(), modtime, rdGzip)
 			return
 		}
 	}
-
-	httpcache.ServeContentWithCacheControl(w, req, fi.Name(), modtime, content)
+	http.ServeContent(w, req, fi.Name(), modtime, content)
 	return
 }
diff --git a/routers/api/v1/repo/file.go b/routers/api/v1/repo/file.go
index 7e6a7ef087..1ba71aa8a3 100644
--- a/routers/api/v1/repo/file.go
+++ b/routers/api/v1/repo/file.go
@@ -83,7 +83,7 @@ func GetRawFile(ctx *context.APIContext) {
 
 	ctx.RespHeader().Set(giteaObjectTypeHeader, string(files_service.GetObjectTypeFromTreeEntry(entry)))
 
-	if err := common.ServeBlob(ctx.Base, ctx.Repo.TreePath, blob, lastModified); err != nil {
+	if err := common.ServeBlob(ctx.Base, ctx.Repo.Repository, ctx.Repo.TreePath, blob, lastModified); err != nil {
 		ctx.APIErrorInternal(err)
 	}
 }
@@ -144,7 +144,7 @@ func GetRawFileOrLFS(ctx *context.APIContext) {
 		}
 
 		// OK not cached - serve!
-		if err := common.ServeBlob(ctx.Base, ctx.Repo.TreePath, blob, lastModified); err != nil {
+		if err := common.ServeBlob(ctx.Base, ctx.Repo.Repository, ctx.Repo.TreePath, blob, lastModified); err != nil {
 			ctx.APIErrorInternal(err)
 		}
 		return
diff --git a/routers/common/errpage.go b/routers/common/errpage.go
index c0b16dbdde..9ca309931b 100644
--- a/routers/common/errpage.go
+++ b/routers/common/errpage.go
@@ -32,7 +32,7 @@ func RenderPanicErrorPage(w http.ResponseWriter, req *http.Request, err any) {
 
 	routing.UpdatePanicError(req.Context(), err)
 
-	httpcache.SetCacheControlInHeader(w.Header(), 0, "no-transform")
+	httpcache.SetCacheControlInHeader(w.Header(), &httpcache.CacheControlOptions{NoTransform: true})
 	w.Header().Set(`X-Frame-Options`, setting.CORSConfig.XFrameOptions)
 
 	tmplCtx := context.TemplateContext{}
diff --git a/routers/common/serve.go b/routers/common/serve.go
index 446908db75..862230b30f 100644
--- a/routers/common/serve.go
+++ b/routers/common/serve.go
@@ -5,17 +5,21 @@ package common
 
 import (
 	"io"
+	"path"
 	"time"
 
+	repo_model "code.gitea.io/gitea/models/repo"
 	"code.gitea.io/gitea/modules/git"
 	"code.gitea.io/gitea/modules/httpcache"
 	"code.gitea.io/gitea/modules/httplib"
 	"code.gitea.io/gitea/modules/log"
+	"code.gitea.io/gitea/modules/setting"
+	"code.gitea.io/gitea/modules/structs"
 	"code.gitea.io/gitea/services/context"
 )
 
 // ServeBlob download a git.Blob
-func ServeBlob(ctx *context.Base, filePath string, blob *git.Blob, lastModified *time.Time) error {
+func ServeBlob(ctx *context.Base, repo *repo_model.Repository, filePath string, blob *git.Blob, lastModified *time.Time) error {
 	if httpcache.HandleGenericETagTimeCache(ctx.Req, ctx.Resp, `"`+blob.ID.String()+`"`, lastModified) {
 		return nil
 	}
@@ -30,14 +34,19 @@ func ServeBlob(ctx *context.Base, filePath string, blob *git.Blob, lastModified
 		}
 	}()
 
-	httplib.ServeContentByReader(ctx.Req, ctx.Resp, filePath, blob.Size(), dataRc)
+	_ = repo.LoadOwner(ctx)
+	httplib.ServeContentByReader(ctx.Req, ctx.Resp, blob.Size(), dataRc, &httplib.ServeHeaderOptions{
+		Filename:      path.Base(filePath),
+		CacheIsPublic: !repo.IsPrivate && repo.Owner != nil && repo.Owner.Visibility == structs.VisibleTypePublic,
+		CacheDuration: setting.StaticCacheTime,
+	})
 	return nil
 }
 
 func ServeContentByReader(ctx *context.Base, filePath string, size int64, reader io.Reader) {
-	httplib.ServeContentByReader(ctx.Req, ctx.Resp, filePath, size, reader)
+	httplib.ServeContentByReader(ctx.Req, ctx.Resp, size, reader, &httplib.ServeHeaderOptions{Filename: path.Base(filePath)})
 }
 
 func ServeContentByReadSeeker(ctx *context.Base, filePath string, modTime *time.Time, reader io.ReadSeeker) {
-	httplib.ServeContentByReadSeeker(ctx.Req, ctx.Resp, filePath, modTime, reader)
+	httplib.ServeContentByReadSeeker(ctx.Req, ctx.Resp, modTime, reader, &httplib.ServeHeaderOptions{Filename: path.Base(filePath)})
 }
diff --git a/routers/web/base.go b/routers/web/base.go
index abe11593f7..a284dd0288 100644
--- a/routers/web/base.go
+++ b/routers/web/base.go
@@ -19,12 +19,12 @@ import (
 	"code.gitea.io/gitea/modules/web/routing"
 )
 
-func storageHandler(storageSetting *setting.Storage, prefix string, objStore storage.ObjectStorage) http.HandlerFunc {
+func avatarStorageHandler(storageSetting *setting.Storage, prefix string, objStore storage.ObjectStorage) http.HandlerFunc {
 	prefix = strings.Trim(prefix, "/")
-	funcInfo := routing.GetFuncInfo(storageHandler, prefix)
+	funcInfo := routing.GetFuncInfo(avatarStorageHandler, prefix)
 
 	if storageSetting.ServeDirect() {
-		return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
+		return func(w http.ResponseWriter, req *http.Request) {
 			if req.Method != "GET" && req.Method != "HEAD" {
 				http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed)
 				return
@@ -52,10 +52,10 @@ func storageHandler(storageSetting *setting.Storage, prefix string, objStore sto
 			}
 
 			http.Redirect(w, req, u.String(), http.StatusTemporaryRedirect)
-		})
+		}
 	}
 
-	return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
+	return func(w http.ResponseWriter, req *http.Request) {
 		if req.Method != "GET" && req.Method != "HEAD" {
 			http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed)
 			return
@@ -93,6 +93,8 @@ func storageHandler(storageSetting *setting.Storage, prefix string, objStore sto
 			return
 		}
 		defer fr.Close()
-		httpcache.ServeContentWithCacheControl(w, req, path.Base(rPath), fi.ModTime(), fr)
-	})
+
+		httpcache.SetCacheControlInHeader(w.Header(), httpcache.CacheControlForPublicStatic())
+		http.ServeContent(w, req, path.Base(rPath), fi.ModTime(), fr)
+	}
 }
diff --git a/routers/web/misc/misc.go b/routers/web/misc/misc.go
index caaca7f521..d42afafe9e 100644
--- a/routers/web/misc/misc.go
+++ b/routers/web/misc/misc.go
@@ -38,7 +38,7 @@ func RobotsTxt(w http.ResponseWriter, req *http.Request) {
 	if ok, _ := util.IsExist(robotsTxt); !ok {
 		robotsTxt = util.FilePathJoinAbs(setting.CustomPath, "robots.txt") // the legacy "robots.txt"
 	}
-	httpcache.SetCacheControlInHeader(w.Header(), setting.StaticCacheTime)
+	httpcache.SetCacheControlInHeader(w.Header(), httpcache.CacheControlForPublicStatic())
 	http.ServeFile(w, req, robotsTxt)
 }
 
diff --git a/routers/web/repo/download.go b/routers/web/repo/download.go
index 060381e9d6..020cebf196 100644
--- a/routers/web/repo/download.go
+++ b/routers/web/repo/download.go
@@ -46,7 +46,7 @@ func ServeBlobOrLFS(ctx *context.Context, blob *git.Blob, lastModified *time.Tim
 				log.Error("ServeBlobOrLFS: Close: %v", err)
 			}
 			closed = true
-			return common.ServeBlob(ctx.Base, ctx.Repo.TreePath, blob, lastModified)
+			return common.ServeBlob(ctx.Base, ctx.Repo.Repository, ctx.Repo.TreePath, blob, lastModified)
 		}
 		if httpcache.HandleGenericETagCache(ctx.Req, ctx.Resp, `"`+pointer.Oid+`"`) {
 			return nil
@@ -78,7 +78,7 @@ func ServeBlobOrLFS(ctx *context.Context, blob *git.Blob, lastModified *time.Tim
 	}
 	closed = true
 
-	return common.ServeBlob(ctx.Base, ctx.Repo.TreePath, blob, lastModified)
+	return common.ServeBlob(ctx.Base, ctx.Repo.Repository, ctx.Repo.TreePath, blob, lastModified)
 }
 
 func getBlobForEntry(ctx *context.Context) (*git.Blob, *time.Time) {
@@ -114,7 +114,7 @@ func SingleDownload(ctx *context.Context) {
 		return
 	}
 
-	if err := common.ServeBlob(ctx.Base, ctx.Repo.TreePath, blob, lastModified); err != nil {
+	if err := common.ServeBlob(ctx.Base, ctx.Repo.Repository, ctx.Repo.TreePath, blob, lastModified); err != nil {
 		ctx.ServerError("ServeBlob", err)
 	}
 }
@@ -142,7 +142,7 @@ func DownloadByID(ctx *context.Context) {
 		}
 		return
 	}
-	if err = common.ServeBlob(ctx.Base, ctx.Repo.TreePath, blob, nil); err != nil {
+	if err = common.ServeBlob(ctx.Base, ctx.Repo.Repository, ctx.Repo.TreePath, blob, nil); err != nil {
 		ctx.ServerError("ServeBlob", err)
 	}
 }
diff --git a/routers/web/repo/wiki.go b/routers/web/repo/wiki.go
index 98c84b6993..0f8e1223c6 100644
--- a/routers/web/repo/wiki.go
+++ b/routers/web/repo/wiki.go
@@ -740,7 +740,7 @@ func WikiRaw(ctx *context.Context) {
 	}
 
 	if entry != nil {
-		if err = common.ServeBlob(ctx.Base, ctx.Repo.TreePath, entry.Blob(), nil); err != nil {
+		if err = common.ServeBlob(ctx.Base, ctx.Repo.Repository, ctx.Repo.TreePath, entry.Blob(), nil); err != nil {
 			ctx.ServerError("ServeBlob", err)
 		}
 		return
diff --git a/routers/web/user/avatar.go b/routers/web/user/avatar.go
index 81c00b3bd4..6d3179bc48 100644
--- a/routers/web/user/avatar.go
+++ b/routers/web/user/avatar.go
@@ -16,7 +16,7 @@ func cacheableRedirect(ctx *context.Context, location string) {
 	// here we should not use `setting.StaticCacheTime`, it is pretty long (default: 6 hours)
 	// we must make sure the redirection cache time is short enough, otherwise a user won't see the updated avatar in 6 hours
 	// it's OK to make the cache time short, it is only a redirection, and doesn't cost much to make a new request
-	httpcache.SetCacheControlInHeader(ctx.Resp.Header(), 5*time.Minute)
+	httpcache.SetCacheControlInHeader(ctx.Resp.Header(), &httpcache.CacheControlOptions{MaxAge: 5 * time.Minute})
 	ctx.Redirect(location)
 }
 
diff --git a/routers/web/web.go b/routers/web/web.go
index 01dc8cf697..3144cb26b2 100644
--- a/routers/web/web.go
+++ b/routers/web/web.go
@@ -233,8 +233,8 @@ func Routes() *web.Router {
 
 	routes.Head("/", misc.DummyOK) // for health check - doesn't need to be passed through gzip handler
 	routes.Methods("GET, HEAD, OPTIONS", "/assets/*", optionsCorsHandler(), public.FileHandlerFunc())
-	routes.Methods("GET, HEAD", "/avatars/*", storageHandler(setting.Avatar.Storage, "avatars", storage.Avatars))
-	routes.Methods("GET, HEAD", "/repo-avatars/*", storageHandler(setting.RepoAvatar.Storage, "repo-avatars", storage.RepoAvatars))
+	routes.Methods("GET, HEAD", "/avatars/*", avatarStorageHandler(setting.Avatar.Storage, "avatars", storage.Avatars))
+	routes.Methods("GET, HEAD", "/repo-avatars/*", avatarStorageHandler(setting.RepoAvatar.Storage, "repo-avatars", storage.RepoAvatars))
 	routes.Methods("GET, HEAD", "/apple-touch-icon.png", misc.StaticRedirect("/assets/img/apple-touch-icon.png"))
 	routes.Methods("GET, HEAD", "/apple-touch-icon-precomposed.png", misc.StaticRedirect("/assets/img/apple-touch-icon.png"))
 	routes.Methods("GET, HEAD", "/favicon.ico", misc.StaticRedirect("/assets/img/favicon.png"))
diff --git a/services/context/api.go b/services/context/api.go
index c163de036c..89280cac80 100644
--- a/services/context/api.go
+++ b/services/context/api.go
@@ -232,7 +232,7 @@ func APIContexter() func(http.Handler) http.Handler {
 				}
 			}
 
-			httpcache.SetCacheControlInHeader(ctx.Resp.Header(), 0, "no-transform")
+			httpcache.SetCacheControlInHeader(ctx.Resp.Header(), &httpcache.CacheControlOptions{NoTransform: true})
 			ctx.Resp.Header().Set(`X-Frame-Options`, setting.CORSConfig.XFrameOptions)
 
 			next.ServeHTTP(ctx.Resp, ctx.Req)
diff --git a/services/context/context.go b/services/context/context.go
index f3a0f0bb5f..79bc5da920 100644
--- a/services/context/context.go
+++ b/services/context/context.go
@@ -191,7 +191,7 @@ func Contexter() func(next http.Handler) http.Handler {
 				}
 			}
 
-			httpcache.SetCacheControlInHeader(ctx.Resp.Header(), 0, "no-transform")
+			httpcache.SetCacheControlInHeader(ctx.Resp.Header(), &httpcache.CacheControlOptions{NoTransform: true})
 			ctx.Resp.Header().Set(`X-Frame-Options`, setting.CORSConfig.XFrameOptions)
 
 			ctx.Data["SystemConfig"] = setting.Config()