Skip to content

Commit

Permalink
Bugfix: cache control response value is now mag-age
Browse files Browse the repository at this point in the history
(as per RFC9111)
  • Loading branch information
rickb777 committed Mar 15, 2023
1 parent 1eb349a commit 40c2111
Show file tree
Hide file tree
Showing 7 changed files with 54 additions and 55 deletions.
10 changes: 7 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,17 @@ Please see the [GoDoc](https://godoc.org/github.com/rickb777/servefiles) for mor
User agents can cache responses. This http server enables easy support for two such mechanisms:

* Conditional requests (using `etags`) allow the response to be sent only when it has changed
* MaxAge response headers allow the user agent to cache entities until some expiry time.
* Cache-Control `max-age` response headers allow the user agent to cache entities until some expiry time.

Note that conditional requests (RFC7232) and MaxAge caching (RFC7234) can work together as required. Conditional requests still require network round trips, whereas caching removes all network round-trips until the entities reach their expiry time.
Note that conditional requests [RFC9110](https://www.rfc-editor.org/rfc/rfc9110#name-conditional-requests) and `max-age` caching ([RFC9111](https://www.rfc-editor.org/rfc/rfc9111#section-5.2.2.1)) can work together as required. Conditional requests still require network round trips, whereas caching removes all network round-trips until the entities reach their expiry time.

## Echo Adapter

Sub-package echo_adapter provides integration hooks into the [Echo web framework](https://echo.labstack.com/). This makes it easy for Echo code to use this asset handler also: see the example in the sub-package for more info.

## Gin Adapter

Sub-package gin_adapter provides integration hooks into the [Gin web framework](github.com/gin-gonic/gin). This makes it easy for Gin code to use this asset handler also: see the example in the sub-package for more info.
Sub-package gin_adapter provides integration hooks into the [Gin web framework](https://github.com/gin-gonic/gin). This makes it easy for Gin code to use this asset handler also: see the example in the sub-package for more info.

## v3

Expand Down
2 changes: 1 addition & 1 deletion assets.go
Original file line number Diff line number Diff line change
Expand Up @@ -226,7 +226,7 @@ func (a *Assets) chooseResource(header http.Header, req *http.Request) (string,

if a.MaxAge > 0 {
header.Set("Expires", a.expires())
header.Set("Cache-Control", fmt.Sprintf("public, maxAge=%d", a.MaxAge/time.Second))
header.Set("Cache-Control", fmt.Sprintf("public, max-age=%d", a.MaxAge/time.Second))
}

acceptEncoding := commaSeparatedList(req.Header.Get("Accept-Encoding"))
Expand Down
60 changes: 30 additions & 30 deletions assets_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ func TestChooseResourceSimpleDirNoGzip(t *testing.T) {
maxAge time.Duration
url, path, cacheControl string
}{
{0, 1, "/", "assets/index.html", "public, maxAge=1"},
{0, 1, "/", "assets/index.html", "public, max-age=1"},
}

for i, test := range cases {
Expand Down Expand Up @@ -89,9 +89,9 @@ func TestChooseResourceSimpleNoGzip(t *testing.T) {
maxAge time.Duration
url, path, cacheControl string
}{
{0, 1, "/img/sort_asc.png", "assets/img/sort_asc.png", "public, maxAge=1"},
{0, 3671, "/img/sort_asc.png", "assets/img/sort_asc.png", "public, maxAge=3671"},
{3, 3671, "/x/y/z/img/sort_asc.png", "assets/img/sort_asc.png", "public, maxAge=3671"},
{0, 1, "/img/sort_asc.png", "assets/img/sort_asc.png", "public, max-age=1"},
{0, 3671, "/img/sort_asc.png", "assets/img/sort_asc.png", "public, max-age=3671"},
{3, 3671, "/x/y/z/img/sort_asc.png", "assets/img/sort_asc.png", "public, max-age=3671"},
}

for i, test := range cases {
Expand Down Expand Up @@ -137,7 +137,7 @@ func TestChooseResourceSimpleNonExistent(t *testing.T) {
//t.Logf("header %v", w.Header())
isGte(t, len(w.Header()), 4, i)
isEqual(t, w.Header().Get("Content-Type"), "text/plain; charset=utf-8", i)
isEqual(t, w.Header().Get("Cache-Control"), "public, maxAge=1", i)
isEqual(t, w.Header().Get("Cache-Control"), "public, max-age=1", i)
isGte(t, len(w.Header().Get("Expires")), 25, i)
}
}
Expand All @@ -148,10 +148,10 @@ func TestServeHTTP200WithGzipAndGzipWithAcceptHeader(t *testing.T) {
maxAge time.Duration
url, mime, encoding, path, cacheControl string
}{
{0, 1, "/css/style1.css", cssMimeType, "xx, gzip, zzz", "assets/css/style1.css.gz", "public, maxAge=1"},
{2, 1, "/a/b/css/style1.css", cssMimeType, "xx, gzip, zzz", "assets/css/style1.css.gz", "public, maxAge=1"},
{0, 1, "/js/script1.js", javascriptMimeType, "xx, gzip, zzz", "assets/js/script1.js.gz", "public, maxAge=1"},
{2, 1, "/a/b/js/script1.js", javascriptMimeType, "xx, gzip, zzz", "assets/js/script1.js.gz", "public, maxAge=1"},
{0, 1, "/css/style1.css", cssMimeType, "xx, gzip, zzz", "assets/css/style1.css.gz", "public, max-age=1"},
{2, 1, "/a/b/css/style1.css", cssMimeType, "xx, gzip, zzz", "assets/css/style1.css.gz", "public, max-age=1"},
{0, 1, "/js/script1.js", javascriptMimeType, "xx, gzip, zzz", "assets/js/script1.js.gz", "public, max-age=1"},
{2, 1, "/a/b/js/script1.js", javascriptMimeType, "xx, gzip, zzz", "assets/js/script1.js.gz", "public, max-age=1"},
}

for _, test := range cases {
Expand Down Expand Up @@ -185,10 +185,10 @@ func TestServeHTTP200WithBrAndBrWithAcceptHeader(t *testing.T) {
maxAge time.Duration
url, mime, encoding, path, cacheControl string
}{
{0, 1, "/css/style1.css", cssMimeType, "br, gzip, zzz", "assets/css/style1.css.br", "public, maxAge=1"},
{2, 1, "/a/b/css/style1.css", cssMimeType, "br, gzip, zzz", "assets/css/style1.css.br", "public, maxAge=1"},
{0, 1, "/js/script1.js", javascriptMimeType, "br, gzip, zzz", "assets/js/script1.js.br", "public, maxAge=1"},
{2, 1, "/a/b/js/script1.js", javascriptMimeType, "br, gzip, zzz", "assets/js/script1.js.br", "public, maxAge=1"},
{0, 1, "/css/style1.css", cssMimeType, "br, gzip, zzz", "assets/css/style1.css.br", "public, max-age=1"},
{2, 1, "/a/b/css/style1.css", cssMimeType, "br, gzip, zzz", "assets/css/style1.css.br", "public, max-age=1"},
{0, 1, "/js/script1.js", javascriptMimeType, "br, gzip, zzz", "assets/js/script1.js.br", "public, max-age=1"},
{2, 1, "/a/b/js/script1.js", javascriptMimeType, "br, gzip, zzz", "assets/js/script1.js.br", "public, max-age=1"},
}

for _, test := range cases {
Expand Down Expand Up @@ -222,10 +222,10 @@ func TestServeHTTP200WithGzipButNoAcceptHeader(t *testing.T) {
maxAge time.Duration
url, mime, encoding, path, cacheControl string
}{
{0, 1, "/css/style1.css", cssMimeType, "xx, yy, zzz", "assets/css/style1.css", "public, maxAge=1"},
{2, 2, "/a/b/css/style1.css", cssMimeType, "xx, yy, zzz", "assets/css/style1.css", "public, maxAge=2"},
{0, 3, "/js/script1.js", javascriptMimeType, "xx, yy, zzz", "assets/js/script1.js", "public, maxAge=3"},
{2, 4, "/a/b/js/script1.js", javascriptMimeType, "xx, yy, zzz", "assets/js/script1.js", "public, maxAge=4"},
{0, 1, "/css/style1.css", cssMimeType, "xx, yy, zzz", "assets/css/style1.css", "public, max-age=1"},
{2, 2, "/a/b/css/style1.css", cssMimeType, "xx, yy, zzz", "assets/css/style1.css", "public, max-age=2"},
{0, 3, "/js/script1.js", javascriptMimeType, "xx, yy, zzz", "assets/js/script1.js", "public, max-age=3"},
{2, 4, "/a/b/js/script1.js", javascriptMimeType, "xx, yy, zzz", "assets/js/script1.js", "public, max-age=4"},
}

for _, test := range cases {
Expand Down Expand Up @@ -258,18 +258,18 @@ func TestServeHTTP200WithGzipAcceptHeaderButNoGzippedFile(t *testing.T) {
maxAge time.Duration
url, mime, encoding, path, cacheControl string
}{
{0, 1, "/css/style2.css", cssMimeType, "xx, gzip, zzz", "assets/css/style2.css", "public, maxAge=1"},
{0, 1, "/css/style2.css", cssMimeType, "br, gzip, zzz", "assets/css/style2.css", "public, maxAge=1"},
{2, 2, "/a/b/css/style2.css", cssMimeType, "xx, gzip, zzz", "assets/css/style2.css", "public, maxAge=2"},
{2, 2, "/a/b/css/style2.css", cssMimeType, "br, gzip, zzz", "assets/css/style2.css", "public, maxAge=2"},
{0, 3, "/js/script2.js", javascriptMimeType, "xx, gzip, zzz", "assets/js/script2.js", "public, maxAge=3"},
{0, 3, "/js/script2.js", javascriptMimeType, "br, gzip, zzz", "assets/js/script2.js", "public, maxAge=3"},
{2, 4, "/a/b/js/script2.js", javascriptMimeType, "xx, gzip, zzz", "assets/js/script2.js", "public, maxAge=4"},
{2, 4, "/a/b/js/script2.js", javascriptMimeType, "br, gzip, zzz", "assets/js/script2.js", "public, maxAge=4"},
{0, 5, "/img/sort_asc.png", "image/png", "xx, gzip, zzz", "assets/img/sort_asc.png", "public, maxAge=5"},
{0, 5, "/img/sort_asc.png", "image/png", "br, gzip, zzz", "assets/img/sort_asc.png", "public, maxAge=5"},
{2, 6, "/a/b/img/sort_asc.png", "image/png", "xx, gzip, zzz", "assets/img/sort_asc.png", "public, maxAge=6"},
{2, 6, "/a/b/img/sort_asc.png", "image/png", "br, gzip, zzz", "assets/img/sort_asc.png", "public, maxAge=6"},
{0, 1, "/css/style2.css", cssMimeType, "xx, gzip, zzz", "assets/css/style2.css", "public, max-age=1"},
{0, 1, "/css/style2.css", cssMimeType, "br, gzip, zzz", "assets/css/style2.css", "public, max-age=1"},
{2, 2, "/a/b/css/style2.css", cssMimeType, "xx, gzip, zzz", "assets/css/style2.css", "public, max-age=2"},
{2, 2, "/a/b/css/style2.css", cssMimeType, "br, gzip, zzz", "assets/css/style2.css", "public, max-age=2"},
{0, 3, "/js/script2.js", javascriptMimeType, "xx, gzip, zzz", "assets/js/script2.js", "public, max-age=3"},
{0, 3, "/js/script2.js", javascriptMimeType, "br, gzip, zzz", "assets/js/script2.js", "public, max-age=3"},
{2, 4, "/a/b/js/script2.js", javascriptMimeType, "xx, gzip, zzz", "assets/js/script2.js", "public, max-age=4"},
{2, 4, "/a/b/js/script2.js", javascriptMimeType, "br, gzip, zzz", "assets/js/script2.js", "public, max-age=4"},
{0, 5, "/img/sort_asc.png", "image/png", "xx, gzip, zzz", "assets/img/sort_asc.png", "public, max-age=5"},
{0, 5, "/img/sort_asc.png", "image/png", "br, gzip, zzz", "assets/img/sort_asc.png", "public, max-age=5"},
{2, 6, "/a/b/img/sort_asc.png", "image/png", "xx, gzip, zzz", "assets/img/sort_asc.png", "public, max-age=6"},
{2, 6, "/a/b/img/sort_asc.png", "image/png", "br, gzip, zzz", "assets/img/sort_asc.png", "public, max-age=6"},
}

for _, test := range cases {
Expand Down Expand Up @@ -401,7 +401,7 @@ func TestServeHTTP304(t *testing.T) {
{"/js/script2.js", "assets/js/script2.js", "xx", &h404{}},
}

// net/http serveFiles handles conditional requests according to RFC723x specs.
// net/http serveFiles handles conditional requests according to RFC9110 specs.
// So we only need to check that a conditional request is correctly wired in.

for i, test := range cases {
Expand Down
29 changes: 12 additions & 17 deletions doc.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,11 @@ Package servefiles provides a static asset handler for serving files such as ima
javascript code. This is an enhancement to the standard net/http ServeFiles, which is used internally.
Care is taken to set headers such that the assets will be efficiently cached by browsers and proxies.
assets := servefiles.NewAssetHandler("./assets/").WithMaxAge(time.Hour)
assets := servefiles.NewAssetHandler("./assets/").WithMaxAge(time.Hour)
Assets is an http.Handler and can be used alongside your other handlers.
Gzipped Content
# Gzipped Content
The Assets handler serves gzipped content when the browser indicates it can accept it. But it does not
gzip anything on-the-fly. Nor does it create any gzipped files for you.
Expand All @@ -47,8 +46,7 @@ You should not attempt to gzip already-compressed files, such as PNG, JPEG, SVGZ
Very small files (e.g. less than 1kb) gain little from compression because they may be small enough to fit
within a single TCP packet, so don't bother with them. (They might even grow in size when gzipped.)
Conditional Request Support
# Conditional Request Support
The Assets handler sets 'Etag' headers for the responses of the assets it finds. Modern browsers need this: they
are then able to send conditional requests that very often shrink responses to a simple 304 Not Modified. This
Expand All @@ -59,10 +57,9 @@ or weak tags are used for plain or gzipped files respectively (the reason is tha
compressed with different levels of compression, a weak Etag indicates there is not a strict match for the
file's content).
For further information see RFC7232 https://tools.ietf.org/html/rfc7232.
For further information see RFC9110 https://tools.ietf.org/html/rfc9110.
Cache Control
# Cache Control
To go even further, the 'far-future' technique can and should often be used. Set a long expiry time, e.g.
ten years via `time.Hour * 24 * 365 * 10`.
Expand All @@ -72,34 +69,32 @@ conditional requests are made. There is clearly a big benefit in page load times
No in-memory caching is performed server-side. This is needed less due to far-future caching being
supported, but might be added in future.
For further information see RFC7234 https://tools.ietf.org/html/rfc7234.
For further information see RFC9111 https://tools.ietf.org/html/rfc9111.
Path Stripping
# Path Stripping
The Assets handler can optionally strip some path segments from the URL before selecting the asset to be served.
This means, for example, that the URL
http://example.com/e3b1cf/css/style1.css
http://example.com/e3b1cf/css/style1.css
can map to the asset files
./assets/css/style1.css
./assets/css/style1.css.gz
./assets/css/style1.css
./assets/css/style1.css.gz
without the /e3b1cf/ segment. The benefit of this is that you can use a unique number or hash in that segment (chosen
for example each time your server starts). Each time that number changes, browsers will see the asset files as
being new, and they will later drop old versions from their cache regardless of their ten-year lifespan.
So you get the far-future lifespan combined with being able to push out changed assets as often as you need to.
Example Usage
# Example Usage
To serve files with a ten-year expiry, this creates a suitably-configured handler:
assets := servefiles.NewAssetHandler("./assets/").StripOff(1).WithMaxAge(10 * 365 * 24 * time.Hour)
assets := servefiles.NewAssetHandler("./assets/").StripOff(1).WithMaxAge(10 * 365 * 24 * time.Hour)
The first parameter names the local directory that holds the asset files. It can be absolute or relative to
the directory in which the server process is started.
Expand Down
2 changes: 1 addition & 1 deletion echo_adapter/handler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ func ExampleHandlerFunc() {

// how long we allow user agents to cache assets
// (this is in addition to conditional requests, see
// RFC7234 https://tools.ietf.org/html/rfc7234#section-5.2.2.8)
// RFC9111 https://www.rfc-editor.org/rfc/rfc9111#section-5.2.2.1)
maxAge := time.Hour

// define the URL pattern that will be routed to the asset handler
Expand Down
4 changes: 2 additions & 2 deletions example_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ func ExampleNewAssetHandler() {

// how long we allow user agents to cache assets
// (this is in addition to conditional requests, see
// RFC7234 https://tools.ietf.org/html/rfc7234#section-5.2.2.8)
// RFC9111 https://www.rfc-editor.org/rfc/rfc9111#section-5.2.2.1)
maxAge := time.Hour

h := servefiles.NewAssetHandler(localPath).WithMaxAge(maxAge)
Expand All @@ -56,7 +56,7 @@ func ExampleNewAssetHandlerFS() {

// how long we allow user agents to cache assets
// (this is in addition to conditional requests, see
// RFC7234 https://tools.ietf.org/html/rfc7234#section-5.2.2.8)
// RFC9111 https://www.rfc-editor.org/rfc/rfc9111#section-5.2.2.1)
maxAge := time.Hour

h := servefiles.NewAssetHandlerFS(fs).WithMaxAge(maxAge)
Expand Down
2 changes: 1 addition & 1 deletion gin_adapter/handler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ func ExampleHandlerFunc() {

// how long we allow user agents to cache assets
// (this is in addition to conditional requests, see
// RFC7234 https://tools.ietf.org/html/rfc7234#section-5.2.2.8)
// RFC9111 https://www.rfc-editor.org/rfc/rfc9111#section-5.2.2.1)
maxAge := time.Hour

// define the URL pattern that will be routed to the asset handler
Expand Down

0 comments on commit 40c2111

Please sign in to comment.