From 6e99f8b301f0db7c9f74c0f8a37eab53c266f10e Mon Sep 17 00:00:00 2001 From: Leon Brocard Date: Wed, 6 May 2026 10:40:34 +0100 Subject: [PATCH] gui: serve static files with gzip/deflate compression Before this change, the GUI server sent all static files uncompressed, meaning the browser had to download the full size of every JS, CSS, and HTML asset. After this change, the GUI server uses chi's Compress middleware at level 5, which negotiates gzip or deflate encoding based on the client's Accept-Encoding header. This reduces transfer sizes significantly for the web UI assets, for example assets/index-CvfdU_RR.js is 874 KB uncompressed, and 265 KB compressed. This is consistent with how rclone serve http, webdav, and restic already compress their responses. --- cmd/gui/gui.go | 2 ++ cmd/gui/gui_test.go | 37 +++++++++++++++++++++++++++++++++++++ 2 files changed, 39 insertions(+) diff --git a/cmd/gui/gui.go b/cmd/gui/gui.go index 0a7ea42d4..03417cc8e 100644 --- a/cmd/gui/gui.go +++ b/cmd/gui/gui.go @@ -14,6 +14,7 @@ import ( "strings" "sync" + "github.com/go-chi/chi/v5/middleware" "github.com/rclone/rclone/cmd" "github.com/rclone/rclone/fs" "github.com/rclone/rclone/fs/rc" @@ -191,6 +192,7 @@ For more help see [the GUI docs](/gui/). if err != nil || spaHandler == nil { return fmt.Errorf("failed to start GUI handler: %w", err) } + guiServer.Router().Use(middleware.Compress(5)) guiServer.Router().Get("/*", spaHandler.ServeHTTP) guiServer.Router().Head("/*", spaHandler.ServeHTTP) guiServer.Serve() diff --git a/cmd/gui/gui_test.go b/cmd/gui/gui_test.go index f6f487d48..942554994 100644 --- a/cmd/gui/gui_test.go +++ b/cmd/gui/gui_test.go @@ -2,6 +2,7 @@ package gui import ( "archive/zip" + "compress/gzip" "io" iofs "io/fs" "net/http" @@ -11,6 +12,8 @@ import ( "strings" "testing" + "github.com/go-chi/chi/v5" + "github.com/go-chi/chi/v5/middleware" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -302,3 +305,37 @@ func TestHandlerSPAFallbackDeepPath(t *testing.T) { assert.Equal(t, http.StatusOK, resp.StatusCode) assert.Contains(t, string(body), "
") } + +func TestHandlerServesGzip(t *testing.T) { + dir := writeTestDir(t) + srcFS, cleanup, err := guiSourceFS(dir) + require.NoError(t, err) + t.Cleanup(func() { _ = cleanup() }) + + h, err := guiHandler(srcFS) + require.NoError(t, err) + + // Build a chi router with Compress middleware, mirroring the + // production setup in the gui command. + r := chi.NewRouter() + r.Use(middleware.Compress(5)) + r.Get("/*", h.ServeHTTP) + + req := httptest.NewRequest("GET", "/", nil) + req.Header.Set("Accept-Encoding", "gzip") + w := httptest.NewRecorder() + r.ServeHTTP(w, req) + + resp := w.Result() + assert.Equal(t, http.StatusOK, resp.StatusCode) + assert.Equal(t, "gzip", resp.Header.Get("Content-Encoding"), + "response should be gzip-encoded when client accepts it") + + // Decompress and verify the content is correct. + gr, err := gzip.NewReader(resp.Body) + require.NoError(t, err) + body, err := io.ReadAll(gr) + require.NoError(t, err) + require.NoError(t, gr.Close()) + assert.Contains(t, string(body), `
`) +}