diff --git a/server/subsonic/api.go b/server/subsonic/api.go index 18ac2da6..6e9af19e 100644 --- a/server/subsonic/api.go +++ b/server/subsonic/api.go @@ -302,8 +302,12 @@ func sendResponse(w http.ResponseWriter, r *http.Request, payload *responses.Sub w.Header().Set("Content-Type", "application/xml") response, err = xml.Marshal(payload) } + // This should never happen, but if it does, we need to know if err != nil { log.Error(r.Context(), "Error marshalling response", "format", f, err) + w.WriteHeader(http.StatusInternalServerError) + _, _ = w.Write([]byte("Internal Server Error: " + err.Error())) + return } if payload.Status == "ok" { if log.IsGreaterOrEqualTo(log.LevelTrace) { diff --git a/server/subsonic/api_test.go b/server/subsonic/api_test.go new file mode 100644 index 00000000..b75af4f9 --- /dev/null +++ b/server/subsonic/api_test.go @@ -0,0 +1,106 @@ +package subsonic + +import ( + "encoding/json" + "encoding/xml" + "math" + "net/http" + "net/http/httptest" + "strings" + + "github.com/navidrome/navidrome/server/subsonic/responses" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +var _ = Describe("sendResponse", func() { + var ( + w *httptest.ResponseRecorder + r *http.Request + payload *responses.Subsonic + ) + + BeforeEach(func() { + w = httptest.NewRecorder() + r = httptest.NewRequest("GET", "/somepath", nil) + payload = &responses.Subsonic{ + Status: "ok", + Version: "1.16.1", + } + }) + + Context("when format is JSON", func() { + It("should set Content-Type to application/json and return the correct body", func() { + q := r.URL.Query() + q.Add("f", "json") + r.URL.RawQuery = q.Encode() + + sendResponse(w, r, payload) + + Expect(w.Header().Get("Content-Type")).To(Equal("application/json")) + Expect(w.Body.String()).NotTo(BeEmpty()) + + var wrapper responses.JsonWrapper + err := json.Unmarshal(w.Body.Bytes(), &wrapper) + Expect(err).NotTo(HaveOccurred()) + Expect(wrapper.Subsonic.Status).To(Equal(payload.Status)) + Expect(wrapper.Subsonic.Version).To(Equal(payload.Version)) + }) + }) + + Context("when format is JSONP", func() { + It("should set Content-Type to application/javascript and return the correct callback body", func() { + q := r.URL.Query() + q.Add("f", "jsonp") + q.Add("callback", "testCallback") + r.URL.RawQuery = q.Encode() + + sendResponse(w, r, payload) + + Expect(w.Header().Get("Content-Type")).To(Equal("application/javascript")) + body := w.Body.String() + Expect(body).To(SatisfyAll( + ContainSubstring("testCallback("), + ContainSubstring(")"), + )) + + // Extract JSON from the JSONP response + jsonBody := body[strings.Index(body, "(")+1 : strings.LastIndex(body, ")")] + var wrapper responses.JsonWrapper + err := json.Unmarshal([]byte(jsonBody), &wrapper) + Expect(err).NotTo(HaveOccurred()) + Expect(wrapper.Subsonic.Status).To(Equal(payload.Status)) + }) + }) + + Context("when format is XML or unspecified", func() { + It("should set Content-Type to application/xml and return the correct body", func() { + // No format specified, expecting XML by default + sendResponse(w, r, payload) + + Expect(w.Header().Get("Content-Type")).To(Equal("application/xml")) + var subsonicResponse responses.Subsonic + err := xml.Unmarshal(w.Body.Bytes(), &subsonicResponse) + Expect(err).NotTo(HaveOccurred()) + Expect(subsonicResponse.Status).To(Equal(payload.Status)) + Expect(subsonicResponse.Version).To(Equal(payload.Version)) + }) + }) + + Context("when an error occurs during marshalling", func() { + It("should return HTTP 500", func() { + payload.Song = &responses.Child{ + ReplayGain: responses.ReplayGain{TrackGain: math.Inf(1)}, + } // This will cause an error when marshalling to JSON + q := r.URL.Query() + q.Add("f", "json") + r.URL.RawQuery = q.Encode() + sendResponse(w, r, payload) + + Expect(w.Code).To(Equal(http.StatusInternalServerError)) + body := w.Body.String() + Expect(body).To(ContainSubstring("Internal Server Error")) + }) + }) + +})