fix(client): log unhandled WebSocket event names instead of empty list

An <updates> frame whose only child is an element the WebSocketEvent
struct doesn't model (e.g. nowSelectionUpdated, sent by SoundTouch 10
firmware around a play action) produced no known event types, so
handleEvent logged "Received unknown event types: []" repeatedly. The
empty list carried no information and flooded soundtouch-web's logs and
the CLI events subscribe output we point people at for debugging.

Capture unmodeled <updates> children by name via an xml:",any" catch-all
on WebSocketEvent and log the actual element names ("[nowSelectionUpdated]"),
skipping frames that carry no child events entirely. A regression test
confirms a modeled event is not also captured as unknown.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Tobias Gesellchen
2026-06-02 23:00:58 +02:00
co-authored by Claude Opus 4.8
parent 040469a074
commit 44d16e54da
3 changed files with 65 additions and 2 deletions
+5 -1
View File
@@ -563,7 +563,11 @@ func (ws *WebSocketClient) handleEvent(event *models.WebSocketEvent) {
if !hasKnownEvent && handlers.OnUnknownEvent != nil {
handlers.OnUnknownEvent(event)
} else if !hasKnownEvent {
ws.logger.Printf("Received unknown event types: %v", eventTypes)
// Log the actual unmodeled element names (e.g. nowSelectionUpdated)
// rather than an empty list; skip frames that carry no child events.
if names := event.UnknownEventNames(); len(names) > 0 {
ws.logger.Printf("Received unhandled event types: %v", names)
}
}
}
+22 -1
View File
@@ -102,7 +102,28 @@ type WebSocketEvent struct {
ErrorUpdated *ErrorUpdatedEvent `xml:"errorUpdated,omitempty"`
RecentsUpdated *RecentsUpdatedEvent `xml:"recentsUpdated,omitempty"`
LanguageUpdated *LanguageUpdatedEvent `xml:"languageUpdated,omitempty"`
Timestamp time.Time `json:"timestamp"` // Added by client for tracking
// UnknownElements captures <updates> children we don't model yet (e.g.
// nowSelectionUpdated), so callers can log them by name instead of an
// empty list when no known event matched.
UnknownElements []UnknownElement `xml:",any"`
Timestamp time.Time `json:"timestamp"` // Added by client for tracking
}
// UnknownElement records the tag name of an <updates> child element that the
// WebSocketEvent struct does not (yet) model.
type UnknownElement struct {
XMLName xml.Name
}
// UnknownEventNames returns the tag names of any unmodeled <updates> children,
// for diagnostic logging.
func (e *WebSocketEvent) UnknownEventNames() []string {
names := make([]string, 0, len(e.UnknownElements))
for _, u := range e.UnknownElements {
names = append(names, u.XMLName.Local)
}
return names
}
// GetEvents returns all events present in this WebSocket event
+38
View File
@@ -540,3 +540,41 @@ func TestCreateMockWebSocketEvent(t *testing.T) {
t.Errorf("Event types don't match expected values")
}
}
// TestParseWebSocketEvent_UnknownElements verifies that an <updates> envelope
// whose only child is an unmodeled element (e.g. nowSelectionUpdated, observed
// on real SoundTouch 10 firmware) parses with no known event types but with
// the element captured by name, so callers can log something useful instead
// of an empty list.
func TestParseWebSocketEvent_UnknownElements(t *testing.T) {
raw := []byte(`<updates deviceID="A81B6A536A98"><nowSelectionUpdated><preset id="0"><ContentItem source="TUNEIN" type="stationurl" location="/v1/playback/station/s308770" sourceAccount="" isPresetable="true"><itemName>Willy</itemName></ContentItem></preset></nowSelectionUpdated></updates>`)
event, err := ParseWebSocketEvent(raw)
if err != nil {
t.Fatalf("ParseWebSocketEvent: %v", err)
}
if got := event.GetEventTypes(); len(got) != 0 {
t.Errorf("expected no known event types, got %v", got)
}
names := event.UnknownEventNames()
if len(names) != 1 || names[0] != "nowSelectionUpdated" {
t.Errorf("expected [nowSelectionUpdated], got %v", names)
}
}
// TestParseWebSocketEvent_KnownEventNoUnknowns confirms a modeled event is not
// also captured as an unknown element.
func TestParseWebSocketEvent_KnownEventNoUnknowns(t *testing.T) {
raw := []byte(`<updates deviceID="A81B6A536A98"><nowPlayingUpdated><nowPlaying deviceID="A81B6A536A98" source="TUNEIN"></nowPlaying></nowPlayingUpdated></updates>`)
event, err := ParseWebSocketEvent(raw)
if err != nil {
t.Fatalf("ParseWebSocketEvent: %v", err)
}
if names := event.UnknownEventNames(); len(names) != 0 {
t.Errorf("expected no unknown elements for a modeled event, got %v", names)
}
}