From 758857b210bbd85f11714843a19671201a4f1110 Mon Sep 17 00:00:00 2001 From: Deluan Date: Wed, 19 Aug 2026 23:45:26 -0400 Subject: [PATCH] fix(server): serve pprof profiles when a BaseURL is configured net/http/pprof's Index resolves the profile name by trimming "/debug/pprof/" from the raw request path, which never matches once MountRouter prepends the BasePath. Requests for any profile without an explicit chi route fell through to the index page, returning HTML with a 200 instead of the profile. The handler now strips the BasePath first. --- cmd/root.go | 9 ++++++++- cmd/root_test.go | 45 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 53 insertions(+), 1 deletion(-) create mode 100644 cmd/root_test.go diff --git a/cmd/root.go b/cmd/root.go index ff1641bd3..5788428ca 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -2,6 +2,7 @@ package cmd import ( "context" + "net/http" "os" "os/signal" "strings" @@ -138,7 +139,7 @@ func startServer(ctx context.Context) func() error { a.MountRouter("Prometheus metrics", conf.Server.Prometheus.MetricsPath, p.GetHandler()) } if conf.Server.DevEnableProfiler { - a.MountRouter("Profiling", "/debug", middleware.Profiler()) + a.MountRouter("Profiling", "/debug", profilerHandler()) } if strings.HasPrefix(conf.Server.UILoginBackgroundURL, "/") { a.MountRouter("Background images", conf.Server.UILoginBackgroundURL, backgrounds.NewHandler()) @@ -147,6 +148,12 @@ func startServer(ctx context.Context) func() error { } } +// profilerHandler returns the pprof handler. net/http/pprof resolves the profile +// name from the raw request path, so the BasePath has to come off first. +func profilerHandler() http.Handler { + return http.StripPrefix(conf.Server.BasePath, middleware.Profiler()) +} + // schedulePeriodicScan schedules a periodic scan of the music library, if configured. func schedulePeriodicScan(ctx context.Context) func() error { return func() error { diff --git a/cmd/root_test.go b/cmd/root_test.go new file mode 100644 index 000000000..eba07cdf8 --- /dev/null +++ b/cmd/root_test.go @@ -0,0 +1,45 @@ +package cmd + +import ( + "net/http" + "net/http/httptest" + "path" + "runtime/pprof" + + "github.com/go-chi/chi/v5" + "github.com/navidrome/navidrome/conf" + "github.com/navidrome/navidrome/conf/configtest" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +var _ = Describe("profilerHandler", func() { + // Mirrors how server.MountRouter mounts the handler. + mount := func() http.Handler { + router := chi.NewRouter() + router.Mount(path.Join(conf.Server.BasePath, "/debug"), profilerHandler()) + return router + } + + BeforeEach(func() { + DeferCleanup(configtest.SetupConfig()) + if pprof.Lookup("nd-profiler-test") == nil { + pprof.NewProfile("nd-profiler-test") + } + }) + + DescribeTable("serves a named profile", + func(basePath string) { + conf.Server.BasePath = basePath + + w := httptest.NewRecorder() + target := path.Join(basePath, "/debug/pprof/nd-profiler-test") + "?debug=1" + mount().ServeHTTP(w, httptest.NewRequest(http.MethodGet, target, nil)) + + Expect(w.Code).To(Equal(http.StatusOK)) + Expect(w.Body.String()).To(HavePrefix("nd-profiler-test profile: total 0")) + }, + Entry("without a BasePath", ""), + Entry("with a BasePath", "/music"), + ) +})