From 69b8193a78383920e68e2430a9de1a475ba658f8 Mon Sep 17 00:00:00 2001 From: Tobias Grieger Date: Tue, 29 Mar 2022 10:19:15 +0200 Subject: [PATCH 1/2] Revert "ui: precompute SHA1 sum for each embedded file" This reverts commit c178f3dde40a68b4bbcb9ddbbfbc2438e3cd50e5. Release note: None --- pkg/ui/buildutil/BUILD.bazel | 16 ------ pkg/ui/buildutil/hash_files.go | 59 --------------------- pkg/ui/buildutil/hash_files_test.go | 81 ----------------------------- pkg/ui/distccl/BUILD.bazel | 1 - pkg/ui/distccl/distccl.go | 8 --- pkg/ui/distccl/distccl_no_bazel.go | 8 --- pkg/ui/distoss/BUILD.bazel | 1 - pkg/ui/distoss/distoss.go | 8 --- pkg/ui/distoss/distoss_no_bazel.go | 10 +--- pkg/ui/ui.go | 4 -- 10 files changed, 1 insertion(+), 195 deletions(-) delete mode 100644 pkg/ui/buildutil/BUILD.bazel delete mode 100644 pkg/ui/buildutil/hash_files.go delete mode 100644 pkg/ui/buildutil/hash_files_test.go diff --git a/pkg/ui/buildutil/BUILD.bazel b/pkg/ui/buildutil/BUILD.bazel deleted file mode 100644 index 702ee7a622e3..000000000000 --- a/pkg/ui/buildutil/BUILD.bazel +++ /dev/null @@ -1,16 +0,0 @@ -load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test") - -go_library( - name = "buildutil", - srcs = ["hash_files.go"], - importpath = "github.com/cockroachdb/cockroach/pkg/ui/buildutil", - visibility = ["//visibility:public"], - deps = ["@com_github_cockroachdb_errors//:errors"], -) - -go_test( - name = "buildutil_test", - srcs = ["hash_files_test.go"], - embed = [":buildutil"], - deps = ["@com_github_stretchr_testify//require"], -) diff --git a/pkg/ui/buildutil/hash_files.go b/pkg/ui/buildutil/hash_files.go deleted file mode 100644 index 8bb677ea6a58..000000000000 --- a/pkg/ui/buildutil/hash_files.go +++ /dev/null @@ -1,59 +0,0 @@ -// Copyright 2022 The Cockroach Authors. -// -// Use of this software is governed by the Business Source License -// included in the file licenses/BSL.txt. -// -// As of the Change Date specified in that file, in accordance with -// the Business Source License, use of this software will be governed -// by the Apache License, Version 2.0, included in the file -// licenses/APL.txt. - -package buildutil - -import ( - "crypto/sha1" - "encoding/hex" - "io" - "io/fs" - - "github.com/cockroachdb/errors" -) - -// HashFilesInDir recursively computes the SHA1 hash of every file in fsys -// starting at root, and stores the computed hashes in dest["/path/to/file"] -// (*including* a leading "/"). -func HashFilesInDir(dest *map[string]string, fsys fs.FS) error { - if dest == nil { - return errors.New("Unable to hash files without a hash destination") - } - - hash := sha1.New() - fileHashes := *dest - - return fs.WalkDir(fsys, ".", func(path string, d fs.DirEntry, err error) error { - if err != nil { - return err - } - - if d.IsDir() { - return nil - } - - hash.Reset() - - file, err := fsys.Open(path) - if err != nil { - return err - } - defer func() { _ = file.Close() }() - - // Copy file contents into the hash algorithm - if _, err := io.Copy(hash, file); err != nil { - return err - } - - // Store the computed hash - fileHashes["/"+path] = hex.EncodeToString(hash.Sum(nil)) - return nil - }) -} diff --git a/pkg/ui/buildutil/hash_files_test.go b/pkg/ui/buildutil/hash_files_test.go deleted file mode 100644 index 55ed5d65f690..000000000000 --- a/pkg/ui/buildutil/hash_files_test.go +++ /dev/null @@ -1,81 +0,0 @@ -// Copyright 2022 The Cockroach Authors. -// -// Use of this software is governed by the Business Source License -// included in the file licenses/BSL.txt. -// -// As of the Change Date specified in that file, in accordance with -// the Business Source License, use of this software will be governed -// by the Apache License, Version 2.0, included in the file -// licenses/APL.txt. - -package buildutil - -import ( - "testing" - "testing/fstest" - - "github.com/stretchr/testify/require" -) - -func TestHashFilesInDir_WithFiles(t *testing.T) { - mapfs := fstest.MapFS{ - "dist/foo.js": &fstest.MapFile{ - Data: []byte("console.log('hello world');"), - }, - "bar.txt": { - Data: []byte("bar.txt contents"), - }, - "lorem/ipsum/dolor.png": { - Data: []byte("pretend this is a png"), - }, - } - fsys, err := mapfs.Sub(".") - require.NoError(t, err) - - expected := map[string]string{ - "/dist/foo.js": "ad43b0d7fb055db16583c156c5507ed58c157e9d", - "/bar.txt": "9b66cb7326bd7d5ded65d24c151438edfcaa5045", - "/lorem/ipsum/dolor.png": "7ee5592b671378807bd078624358d5140c6d8512", - } - - hashes := make(map[string]string) - result := HashFilesInDir(&hashes, fsys) - - require.NoError(t, result) - require.EqualValues(t, expected, hashes) - require.NotEmpty(t, hashes) -} - -func TestHashFilesInDir_EmptyFS(t *testing.T) { - mapfs := fstest.MapFS{} - fsys, err := mapfs.Sub(".") - require.NoError(t, err) - - expected := map[string]string{} - - hashes := make(map[string]string) - result := HashFilesInDir(&hashes, fsys) - - require.NoError(t, result) - require.EqualValues(t, expected, hashes) - require.Empty(t, hashes) -} - -func TestHashFilesInDir_NilMap(t *testing.T) { - mapfs := fstest.MapFS{ - "dist/foo.js": &fstest.MapFile{ - Data: []byte("console.log('hello world');"), - }, - "bar.txt": { - Data: []byte("bar.txt contents"), - }, - "lorem/ipsum/dolor.png": { - Data: []byte("pretend this is a png"), - }, - } - fsys, err := mapfs.Sub(".") - require.NoError(t, err) - - result := HashFilesInDir(nil, fsys) - require.Errorf(t, result, "Unable to hash files without a hash destination") -} diff --git a/pkg/ui/distccl/BUILD.bazel b/pkg/ui/distccl/BUILD.bazel index e96fe06f7bc8..36141cea44a8 100644 --- a/pkg/ui/distccl/BUILD.bazel +++ b/pkg/ui/distccl/BUILD.bazel @@ -26,7 +26,6 @@ go_library( visibility = ["//visibility:public"], deps = [ "//pkg/ui", - "//pkg/ui/buildutil", "//pkg/util/targz", ], ) diff --git a/pkg/ui/distccl/distccl.go b/pkg/ui/distccl/distccl.go index ae1528f7a7a2..b215712cfe42 100644 --- a/pkg/ui/distccl/distccl.go +++ b/pkg/ui/distccl/distccl.go @@ -19,7 +19,6 @@ import ( _ "embed" "github.com/cockroachdb/cockroach/pkg/ui" - "github.com/cockroachdb/cockroach/pkg/ui/buildutil" "github.com/cockroachdb/cockroach/pkg/util/targz" ) @@ -33,11 +32,4 @@ func init() { } ui.Assets = fs ui.HaveUI = true - - assetHashes := make(map[string]string) - err = buildutil.HashFilesInDir(&assetHashes, ui.Assets) - if err != nil { - panic(err) - } - ui.AssetHashes = assetHashes } diff --git a/pkg/ui/distccl/distccl_no_bazel.go b/pkg/ui/distccl/distccl_no_bazel.go index 950995dcaae2..6489d2289706 100644 --- a/pkg/ui/distccl/distccl_no_bazel.go +++ b/pkg/ui/distccl/distccl_no_bazel.go @@ -19,7 +19,6 @@ import ( "io/fs" "github.com/cockroachdb/cockroach/pkg/ui" - "github.com/cockroachdb/cockroach/pkg/ui/buildutil" ) //go:embed assets/* @@ -32,11 +31,4 @@ func init() { panic(err) } ui.HaveUI = true - - assetHashes := make(map[string]string) - err = buildutil.HashFilesInDir(&assetHashes, ui.Assets) - if err != nil { - panic(err) - } - ui.AssetHashes = assetHashes } diff --git a/pkg/ui/distoss/BUILD.bazel b/pkg/ui/distoss/BUILD.bazel index e4465b9b1ff7..228004aa01eb 100644 --- a/pkg/ui/distoss/BUILD.bazel +++ b/pkg/ui/distoss/BUILD.bazel @@ -26,7 +26,6 @@ go_library( visibility = ["//visibility:public"], deps = [ "//pkg/ui", - "//pkg/ui/buildutil", "//pkg/util/targz", ], ) diff --git a/pkg/ui/distoss/distoss.go b/pkg/ui/distoss/distoss.go index 87dbbe85d47d..92e34b69f598 100644 --- a/pkg/ui/distoss/distoss.go +++ b/pkg/ui/distoss/distoss.go @@ -21,7 +21,6 @@ import ( _ "embed" "github.com/cockroachdb/cockroach/pkg/ui" - "github.com/cockroachdb/cockroach/pkg/ui/buildutil" "github.com/cockroachdb/cockroach/pkg/util/targz" ) @@ -35,11 +34,4 @@ func init() { } ui.Assets = fs ui.HaveUI = true - - assetHashes := make(map[string]string) - err = buildutil.HashFilesInDir(&assetHashes, ui.Assets) - if err != nil { - panic(err) - } - ui.AssetHashes = assetHashes } diff --git a/pkg/ui/distoss/distoss_no_bazel.go b/pkg/ui/distoss/distoss_no_bazel.go index 62ac6e61d27a..ed4e82adc716 100644 --- a/pkg/ui/distoss/distoss_no_bazel.go +++ b/pkg/ui/distoss/distoss_no_bazel.go @@ -21,7 +21,6 @@ import ( "io/fs" "github.com/cockroachdb/cockroach/pkg/ui" - "github.com/cockroachdb/cockroach/pkg/ui/buildutil" ) //go:embed assets/* @@ -29,16 +28,9 @@ var assets embed.FS func init() { var err error - ui.HaveUI = true ui.Assets, err = fs.Sub(assets, "assets") if err != nil { panic(err) } - - assetHashes := make(map[string]string) - err = buildutil.HashFilesInDir(&assetHashes, ui.Assets) - if err != nil { - panic(err) - } - ui.AssetHashes = assetHashes + ui.HaveUI = true } diff --git a/pkg/ui/ui.go b/pkg/ui/ui.go index d61ae5e0fbb6..fe6673862160 100644 --- a/pkg/ui/ui.go +++ b/pkg/ui/ui.go @@ -39,10 +39,6 @@ var Assets fs.FS // HaveUI tells whether the admin UI has been linked into the binary. var HaveUI = false -// AssetHashes is used to provide a unique per-file checksum for each served file, -// which enables client-side caching using Cache-Control and ETag headers. -var AssetHashes map[string]string - // indexTemplate takes arguments about the current session and returns HTML // which includes the UI JavaScript bundles, plus a script tag which sets the // currently logged in user so that the UI JavaScript can decide whether to show From 39a600e1aa630f461358db7465f6d75425c7b748 Mon Sep 17 00:00:00 2001 From: Tobias Grieger Date: Tue, 29 Mar 2022 10:19:24 +0200 Subject: [PATCH 2/2] Revert "ui: serve ETag header and respect If-None-Match req. header for assets" This reverts commit b43ca1d485455e19919646699a2061ee2bb1be21. Release note: None --- pkg/BUILD.bazel | 1 - pkg/server/server_test.go | 83 ---------------- pkg/ui/BUILD.bazel | 1 - pkg/ui/ui.go | 10 +- pkg/util/httputil/BUILD.bazel | 14 +-- pkg/util/httputil/etag_handler_test.go | 131 ------------------------- pkg/util/httputil/handlers.go | 72 -------------- 7 files changed, 3 insertions(+), 309 deletions(-) delete mode 100644 pkg/util/httputil/etag_handler_test.go delete mode 100644 pkg/util/httputil/handlers.go diff --git a/pkg/BUILD.bazel b/pkg/BUILD.bazel index dfbf7b55a9d8..a278caeebdd6 100644 --- a/pkg/BUILD.bazel +++ b/pkg/BUILD.bazel @@ -408,7 +408,6 @@ ALL_TESTS = [ "//pkg/util/goschedstats:goschedstats_test", "//pkg/util/grpcutil:grpcutil_test", "//pkg/util/hlc:hlc_test", - "//pkg/util/httputil:httputil_test", "//pkg/util/humanizeutil:humanizeutil_test", "//pkg/util/interval/generic:generic_test", "//pkg/util/interval:interval_test", diff --git a/pkg/server/server_test.go b/pkg/server/server_test.go index d56faf73d9a2..0d5d0a19e9f5 100644 --- a/pkg/server/server_test.go +++ b/pkg/server/server_test.go @@ -26,7 +26,6 @@ import ( "regexp" "strings" "testing" - "testing/fstest" "time" "github.com/cockroachdb/cockroach/pkg/base" @@ -957,88 +956,6 @@ Binary built without web UI. }) } }) - - t.Run("Client-side caching", func(t *testing.T) { - linkInFakeUI() - defer unlinkFakeUI() - - // Set up fake asset FS with hashes - mapfs := fstest.MapFS{ - "bundle.js": &fstest.MapFile{ - Data: []byte("console.log('hello world');"), - }, - } - fsys, err := mapfs.Sub(".") - require.NoError(t, err) - ui.Assets = fsys - ui.AssetHashes = map[string]string{ - "/bundle.js": "ad43b0d7fb055db16583c156c5507ed58c157e9d", - } - - // Clear fake asset FS and hashes when we're done - defer func() { - ui.Assets = nil - ui.AssetHashes = nil - }() - - s, _, _ := serverutils.StartServer(t, base.TestServerArgs{}) - defer s.Stopper().Stop(ctx) - tsrv := s.(*TestServer) - - loggedInClient, err := tsrv.GetAdminHTTPClient() - require.NoError(t, err) - loggedOutClient, err := tsrv.GetUnauthenticatedHTTPClient() - require.NoError(t, err) - - cases := []struct { - desc string - client http.Client - }{ - { - desc: "unauthenticated user", - client: loggedOutClient, - }, - { - desc: "authenticated user", - client: loggedInClient, - }, - } - - for _, testCase := range cases { - t.Run(fmt.Sprintf("bundle caching for %s", testCase.desc), func(t *testing.T) { - // Request bundle.js without an If-None-Match header first, to simulate the initial load - uncachedReq, err := http.NewRequestWithContext(ctx, "GET", s.AdminURL()+"/bundle.js", nil) - require.NoError(t, err) - - uncachedResp, err := testCase.client.Do(uncachedReq) - require.NoError(t, err) - defer uncachedResp.Body.Close() - require.Equal(t, 200, uncachedResp.StatusCode) - - etag := uncachedResp.Header.Get("ETag") - require.NotEmpty(t, etag, "Server must provide ETag response header with asset responses") - - // Use that ETag header on the next request to simulate a client reload - cachedReq, err := http.NewRequestWithContext(ctx, "GET", s.AdminURL()+"/bundle.js", nil) - require.NoError(t, err) - cachedReq.Header.Add("If-None-Match", etag) - - cachedResp, err := testCase.client.Do(cachedReq) - require.NoError(t, err) - defer cachedResp.Body.Close() - require.Equal(t, 304, cachedResp.StatusCode) - - respBytes, err := ioutil.ReadAll(cachedResp.Body) - require.NoError(t, err) - require.Empty(t, respBytes, "Server must provide empty body for cached response") - - etagFromEmptyResp := cachedResp.Header.Get("ETag") - require.NotEmpty(t, etag, "Server must provide ETag response header with asset responses") - - require.Equal(t, etag, etagFromEmptyResp, "Server must provide consistent ETag response headers") - }) - } - }) } func TestGWRuntimeMarshalProto(t *testing.T) { diff --git a/pkg/ui/BUILD.bazel b/pkg/ui/BUILD.bazel index 4591682a6dfa..e530c918398f 100644 --- a/pkg/ui/BUILD.bazel +++ b/pkg/ui/BUILD.bazel @@ -19,7 +19,6 @@ go_library( deps = [ "//pkg/base", "//pkg/build", - "//pkg/util/httputil", "//pkg/util/log", "@com_github_cockroachdb_errors//:errors", ], diff --git a/pkg/ui/ui.go b/pkg/ui/ui.go index fe6673862160..902ac58a6c29 100644 --- a/pkg/ui/ui.go +++ b/pkg/ui/ui.go @@ -26,7 +26,6 @@ import ( "github.com/cockroachdb/cockroach/pkg/base" "github.com/cockroachdb/cockroach/pkg/build" - "github.com/cockroachdb/cockroach/pkg/util/httputil" "github.com/cockroachdb/cockroach/pkg/util/log" "github.com/cockroachdb/errors" ) @@ -113,12 +112,7 @@ type Config struct { // including index.html, which has some login-related variables // templated into it, as well as static assets. func Handler(cfg Config) http.Handler { - handlerChain := httputil.EtagHandler( - AssetHashes, - http.FileServer( - http.FS(Assets), - ), - ) + fileServer := http.FileServer(http.FS(Assets)) buildInfo := build.GetInfo() return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { @@ -128,7 +122,7 @@ func Handler(cfg Config) http.Handler { } if r.URL.Path != "/" { - handlerChain.ServeHTTP(w, r) + fileServer.ServeHTTP(w, r) return } diff --git a/pkg/util/httputil/BUILD.bazel b/pkg/util/httputil/BUILD.bazel index decf7214801c..61f937dadc37 100644 --- a/pkg/util/httputil/BUILD.bazel +++ b/pkg/util/httputil/BUILD.bazel @@ -1,28 +1,16 @@ -load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test") +load("@io_bazel_rules_go//go:def.bzl", "go_library") go_library( name = "httputil", srcs = [ "client.go", - "handlers.go", "http.go", ], importpath = "github.com/cockroachdb/cockroach/pkg/util/httputil", visibility = ["//visibility:public"], deps = [ - "//pkg/util/log", "//pkg/util/protoutil", "@com_github_cockroachdb_errors//:errors", "@com_github_gogo_protobuf//jsonpb", ], ) - -go_test( - name = "httputil_test", - srcs = ["etag_handler_test.go"], - embed = [":httputil"], - deps = [ - "//pkg/util/leaktest", - "@com_github_stretchr_testify//require", - ], -) diff --git a/pkg/util/httputil/etag_handler_test.go b/pkg/util/httputil/etag_handler_test.go deleted file mode 100644 index eb7d407c9047..000000000000 --- a/pkg/util/httputil/etag_handler_test.go +++ /dev/null @@ -1,131 +0,0 @@ -// Copyright 2022 The Cockroach Authors. -// -// Use of this software is governed by the Business Source License -// included in the file licenses/BSL.txt. -// -// As of the Change Date specified in that file, in accordance with -// the Business Source License, use of this software will be governed -// by the Apache License, Version 2.0, included in the file -// licenses/APL.txt. - -package httputil - -import ( - "fmt" - "io" - "net/http" - "net/http/httptest" - "net/url" - "testing" - - "github.com/cockroachdb/cockroach/pkg/util/leaktest" - "github.com/stretchr/testify/require" -) - -type testCase struct { - desc string - path string - ifNoneMatch string - expectedStatusCode int -} - -func mustParseURL(unparsed string) *url.URL { - out, err := url.Parse(unparsed) - if err != nil { - panic(err) - } - - return out -} - -func TestEtagHandler(t *testing.T) { - defer leaktest.AfterTest(t)() - - okHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - _, err := w.Write([]byte("(http response body)")) - require.NoError(t, err, "HTTP handler that always returns 200 failed to write response. Something's very wrong.") - }) - - // The hashes here aren't significant, as long as they're sent in client requests - hashedFiles := map[string]string{ - "/dist/hello.js": "0123111", - "/lorem/ipsum/dolor.png": "4567222", - "/README.md": "789afff", - } - - handler := EtagHandler(hashedFiles, okHandler) - server := httptest.NewServer(handler) - defer server.Close() - client := server.Client() - - cases := []testCase{ - { - desc: "matching ETag", - path: "/README.md", - ifNoneMatch: `"789afff"`, - expectedStatusCode: 304, - }, - { - desc: "matching but malformed ETag (missing quotes)", - path: "/README.md", - ifNoneMatch: `789afff`, // Note: no doublequotes around this hash! - expectedStatusCode: 200, - }, - { - desc: "mismatched ETag", - path: "/README.md", - ifNoneMatch: `"not the right etag"`, - expectedStatusCode: 200, - }, - { - desc: "no ETag", - path: "/README.md", - expectedStatusCode: 200, - }, - { - desc: "unhashed file", - path: "/this/file/isnt/hashed.css", - ifNoneMatch: `"5ca1eab1e"`, - expectedStatusCode: 200, - }, - } - - for _, tc := range cases { - tc := tc - t.Run(fmt.Sprintf("request to %s with %s", tc.path, tc.desc), func(t *testing.T) { - tmp := mustParseURL(server.URL + tc.path) - fmt.Printf("GETing url '%s'\n", tmp) - resp, err := client.Do(&http.Request{ - URL: mustParseURL(server.URL + tc.path), - Header: http.Header{ - "If-None-Match": []string{tc.ifNoneMatch}, - }, - }) - - require.NoError(t, err) - defer resp.Body.Close() - require.Equal(t, tc.expectedStatusCode, resp.StatusCode) - - checksum, checksumExists := hashedFiles[tc.path] - // Requests for files with ETags must always include the ETag in the response - if checksumExists { - require.Equal( - t, - `"`+checksum+`"`, - resp.Header.Get("ETag"), - "Requests for hashed files must always include an ETag response header", - ) - } - - bodyBytes, err := io.ReadAll(resp.Body) - require.NoError(t, err) - body := string(bodyBytes) - - if tc.expectedStatusCode == 304 { - require.Empty(t, body) - } else if tc.expectedStatusCode == 200 { - require.Equal(t, "(http response body)", body) - } - }) - } -} diff --git a/pkg/util/httputil/handlers.go b/pkg/util/httputil/handlers.go deleted file mode 100644 index c7318a42d21c..000000000000 --- a/pkg/util/httputil/handlers.go +++ /dev/null @@ -1,72 +0,0 @@ -// Copyright 2022 The Cockroach Authors. -// -// Use of this software is governed by the Business Source License -// included in the file licenses/BSL.txt. -// -// As of the Change Date specified in that file, in accordance with -// the Business Source License, use of this software will be governed -// by the Apache License, Version 2.0, included in the file -// licenses/APL.txt. - -package httputil - -import ( - "net/http" - - "github.com/cockroachdb/cockroach/pkg/util/log" -) - -// EtagHandler creates an http.Handler middleware that wraps another HTTP -// handler, adding support for the If-None-Match request header and ETag -// response header based on pre-computed file hashes. All responses include an -// ETag header with the hash provided in contentHashes. When a client provides -// an If-None-Match header with the hash found in contentHashes, no file is -// served and an HTTP 304 with no body is sent to clients instead, to indicate -// that the client's stale cache entry is still valid. -// -// - contentHashes is a map of URL path (including a leading "/") to the ETag -// value to use for that file -// - next is the next handler in the http.Handler chain, used -func EtagHandler(contentHashes map[string]string, next http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - if contentHashes == nil { - // If the hashed content map is erased, turn this into a no-op handler. - next.ServeHTTP(w, r) - return - } - - ifNoneMatch := r.Header.Get("If-None-Match") - checksum, checksumFound := contentHashes[r.URL.Path] - // ETag header values are always wrapped in double-quotes - wrappedChecksum := `"` + checksum + `"` - - if checksumFound { - // Always add the ETag header for assets that support hash-based caching. - // - // * If the client requested the asset with the correct has in the - // If-None-Match header, its cache is stale! Returning the ETag again is - // required to indicate which hash should be used for the next request. - // * If the client requested the asset with no If-None-Match header or an - // incorrect If-None-Match header, the content has changed since the - // last value and must be served with its identifying hash. - w.Header().Add("ETag", wrappedChecksum) - } - - if ifNoneMatch != "" && wrappedChecksum == ifNoneMatch { - // The client still has this asset cached, but its cache is stale. - // Return 304 with no body to tell the client that its cached version is - // still fresh, and that it can use the provided ETag for its next - // request. - w.WriteHeader(304) - if _, err := w.Write(nil); err != nil { - log.Errorf(r.Context(), "Unable to write empty response body: %+v", err) - } - return - } - - // Either the client didn't send the correct hash, sent no hash, or the - // requested asset isn't eligible for hash-based caching. Pass this - // request to the next handler in the chain. - next.ServeHTTP(w, r) - }) -}