diff --git a/README.md b/README.md index f985edd..c895342 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/assets.go b/assets.go index 12c4739..afef9e9 100644 --- a/assets.go +++ b/assets.go @@ -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")) diff --git a/assets_test.go b/assets_test.go index 85efcbc..10a6b58 100644 --- a/assets_test.go +++ b/assets_test.go @@ -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 { @@ -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 { @@ -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) } } @@ -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 { @@ -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 { @@ -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 { @@ -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 { @@ -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 { diff --git a/doc.go b/doc.go index 732e555..c2924f3 100644 --- a/doc.go +++ b/doc.go @@ -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. @@ -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 @@ -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`. @@ -72,21 +69,20 @@ 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 @@ -94,12 +90,11 @@ being new, and they will later drop old versions from their cache regardless of 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. diff --git a/echo_adapter/handler_test.go b/echo_adapter/handler_test.go index dae0a23..5587a6b 100644 --- a/echo_adapter/handler_test.go +++ b/echo_adapter/handler_test.go @@ -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 diff --git a/example_test.go b/example_test.go index bfbfbeb..92720b1 100644 --- a/example_test.go +++ b/example_test.go @@ -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) @@ -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) diff --git a/gin_adapter/handler_test.go b/gin_adapter/handler_test.go index c77ef03..b97e48e 100644 --- a/gin_adapter/handler_test.go +++ b/gin_adapter/handler_test.go @@ -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