mirror of
https://github.com/truewhile/MeBox.git
synced 2026-09-28 03:06:38 +08:00
fix(dlna): always return non-nil device slice so API serializes [] not null
The DLNA device list endpoint could return {"devices":null} in two cases:
the SSDP-failure path returned a nil slice, and the cached path copied an
empty cache via append([]DLNADevice(nil), cache...) which also yields nil.
The web UI reads res.devices.length, so a null devices field crashed the
DLNA page to a white screen. Return non-nil slices in both paths and guard
the frontend with a null coalesce. Adds a regression test.
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
@@ -114,7 +114,8 @@ func (d *DLNAService) ssdpDiscover(ctx context.Context, timeout time.Duration) (
|
|||||||
func (d *DLNAService) Discover(ctx context.Context, force bool) ([]DLNADevice, error) {
|
func (d *DLNAService) Discover(ctx context.Context, force bool) ([]DLNADevice, error) {
|
||||||
d.mu.Lock()
|
d.mu.Lock()
|
||||||
if !force && time.Since(d.cachedAt) < 30*time.Second && d.cache != nil {
|
if !force && time.Since(d.cachedAt) < 30*time.Second && d.cache != nil {
|
||||||
out := append([]DLNADevice(nil), d.cache...)
|
// Copy into a non-nil slice so an empty cache serializes as [] not null.
|
||||||
|
out := append(make([]DLNADevice, 0, len(d.cache)), d.cache...)
|
||||||
d.mu.Unlock()
|
d.mu.Unlock()
|
||||||
return out, nil
|
return out, nil
|
||||||
}
|
}
|
||||||
@@ -123,9 +124,10 @@ func (d *DLNAService) Discover(ctx context.Context, force bool) ([]DLNADevice, e
|
|||||||
locations, err := d.ssdpDiscover(ctx, 3*time.Second)
|
locations, err := d.ssdpDiscover(ctx, 3*time.Second)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// SSDP often fails on container networks; treat as "no devices"
|
// SSDP often fails on container networks; treat as "no devices"
|
||||||
// rather than 500 the API.
|
// rather than 500 the API. Return an empty (non-nil) slice so the
|
||||||
|
// JSON response is [] not null.
|
||||||
d.log.Debug("ssdp discover failed", zap.Error(err))
|
d.log.Debug("ssdp discover failed", zap.Error(err))
|
||||||
return nil, nil
|
return []DLNADevice{}, nil
|
||||||
}
|
}
|
||||||
devices := make([]DLNADevice, 0, len(locations))
|
devices := make([]DLNADevice, 0, len(locations))
|
||||||
for _, loc := range locations {
|
for _, loc := range locations {
|
||||||
|
|||||||
@@ -0,0 +1,37 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"go.uber.org/zap"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TestDLNADiscoverCachedEmptyReturnsNonNil guards against the bug where an
|
||||||
|
// empty device cache was copied via append([]DLNADevice(nil), cache...),
|
||||||
|
// which yields a nil slice and serializes to JSON null. The web UI reads
|
||||||
|
// res.devices.length, so a null devices field crashed the DLNA page.
|
||||||
|
func TestDLNADiscoverCachedEmptyReturnsNonNil(t *testing.T) {
|
||||||
|
d := NewDLNAService(zap.NewNop())
|
||||||
|
// Prime the cache with an empty (but non-nil) result, fresh enough to be served.
|
||||||
|
d.cache = []DLNADevice{}
|
||||||
|
d.cachedAt = time.Now()
|
||||||
|
|
||||||
|
out, err := d.Discover(context.Background(), false)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Discover returned error: %v", err)
|
||||||
|
}
|
||||||
|
if out == nil {
|
||||||
|
t.Fatal("Discover returned nil slice for empty cache; want non-nil so JSON is [] not null")
|
||||||
|
}
|
||||||
|
|
||||||
|
b, err := json.Marshal(map[string]any{"devices": out})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("marshal: %v", err)
|
||||||
|
}
|
||||||
|
if got := string(b); got != `{"devices":[]}` {
|
||||||
|
t.Fatalf("devices serialized as %s; want {\"devices\":[]}", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
+2
-2
@@ -13,8 +13,8 @@ export interface DLNADevice {
|
|||||||
export const dlnaAPI = {
|
export const dlnaAPI = {
|
||||||
list: (force = false) =>
|
list: (force = false) =>
|
||||||
api
|
api
|
||||||
.get<{ devices: DLNADevice[] }>('/dlna/devices', { params: { force: force ? 'true' : '' } })
|
.get<{ devices: DLNADevice[] | null }>('/dlna/devices', { params: { force: force ? 'true' : '' } })
|
||||||
.then((r) => r.data.devices),
|
.then((r) => r.data.devices ?? []),
|
||||||
|
|
||||||
cast: (controlURL: string, mediaURL: string) =>
|
cast: (controlURL: string, mediaURL: string) =>
|
||||||
api
|
api
|
||||||
|
|||||||
Reference in New Issue
Block a user