From 76a1b946ebee341bca209001f596364637c42ff7 Mon Sep 17 00:00:00 2001 From: Sebastian Date: Wed, 7 Oct 2026 20:09:30 +0200 Subject: [PATCH] Stop browsers from caching songs /music/rand and the other song endpoints return a different song from the same URL, but the responses had a Last-Modified header and no Cache-Control, so browsers cached them. The same song played again and the server never picked a new one, so /music/addQue added nothing. Send Cache-Control: no-store again, as the server did before the rewrite. Also allow the Cache-Control request header in CORS, which the frontend sends to bypass responses that browsers have already cached. --- internal/server/files.go | 14 +++++++++++++- internal/server/handlers_test.go | 13 +++++++++++++ internal/server/routes.go | 2 +- 3 files changed, 27 insertions(+), 2 deletions(-) diff --git a/internal/server/files.go b/internal/server/files.go index 44c3432..77d7dab 100644 --- a/internal/server/files.go +++ b/internal/server/files.go @@ -52,9 +52,21 @@ func streamOpenFile(f *os.File, contentType string, audio bool) (*huma.StreamRes } // streamSong streams the song at path, or returns err mapped to an API error. +// The same URL returns a different song each time (random songs, next, previous), +// so browsers must not cache it. Without this they cache the response because of +// its Last-Modified header, and play the same song again. func streamSong(path string, err error) (*huma.StreamResponse, error) { if err != nil { return nil, apiError(err) } - return streamFile(path, "audio/mpeg", true) + resp, err := streamFile(path, "audio/mpeg", true) + if err != nil { + return nil, err + } + body := resp.Body + resp.Body = func(ctx huma.Context) { + ctx.SetHeader("Cache-Control", "no-store") + body(ctx) + } + return resp, nil } diff --git a/internal/server/handlers_test.go b/internal/server/handlers_test.go index c5e3d9c..e613851 100644 --- a/internal/server/handlers_test.go +++ b/internal/server/handlers_test.go @@ -92,6 +92,19 @@ func TestMusicQueueEndsAfterSync(t *testing.T) { assert.JSONEq(t, "[]", rec.Body.String()) } +func TestMusicSongsAreNotCached(t *testing.T) { + e := newTestEnv(t) + e.addSong("Game A", "Song", "x") + e.do(http.MethodGet, "/api/v1/music/rand") + e.do(http.MethodGet, "/api/v1/music/addQue") + for _, path := range []string{"/api/v1/music/rand", "/api/v1/music/rand/low", "/api/v1/music/rand/classic", + "/api/v1/music/next", "/api/v1/music/previous", "/api/v1/music?song=0", "/api/v1/music/soundTest"} { + rec := e.do(http.MethodGet, path) + require.Equal(t, http.StatusOK, rec.Code, path) + assert.Equal(t, "no-store", rec.Header().Get("Cache-Control"), path) + } +} + func TestMusicRangeRequest(t *testing.T) { e := newTestEnv(t) e.addSong("Game A", "Song", "0123456789") diff --git a/internal/server/routes.go b/internal/server/routes.go index 9602522..3d4fc0d 100644 --- a/internal/server/routes.go +++ b/internal/server/routes.go @@ -46,7 +46,7 @@ func (s *Server) routes() (*echo.Echo, huma.API) { e.Use(echoMiddleware.CORSWithConfig(echoMiddleware.CORSConfig{ AllowOrigins: s.cfg.CORSAllowedOrigins, AllowMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS", "PATCH"}, - AllowHeaders: []string{"Accept", "Authorization", "Content-Type", "Range", middleware.QueueHeader}, + AllowHeaders: []string{"Accept", "Authorization", "Cache-Control", "Content-Type", "Range", middleware.QueueHeader}, ExposeHeaders: []string{middleware.QueueHeader, "Content-Range", "Accept-Ranges"}, MaxAge: 300, }))