v2026.06.03 hotfix — Sonos cast URL + UPnP picker polish #78

Merged
bvandeusen merged 4 commits from dev into main 2026-06-03 15:30:09 -04:00
Owner

Hotfix for v2026.06.03's UPnP path. On-device verification with a real Sonos speaker uncovered three issues; this PR ships all three plus the operator-diagnostic plumbing that made the root-cause findable.

Root cause — UPnP error 714 from Sonos (PRIMARY FIX)

On-device tap of a Sonos device dismissed the picker sheet but no audio routed. Logcat:

UPnP select: mint token for track=da85199c-..., route=Living Room
POST /api/cast/stream-token  → 200 (token minted)
SetAVTransportURI to http://minstrel.fabledsword.com/api/tracks/.../stream?token=...&exp=...
<-- 500 from Sonos
SoapFaultException: SOAP fault 714 (IllegalMimeType)

UPnP error 714 = "Illegal MIME-Type" — Sonos does a HEAD/probe to detect the audio MIME at the URL. Our cast_token.go URL builder trusted r.TLS for the scheme, which is nil behind a TLS-terminating reverse proxy, so the URL came out http://. Sonos hit http:// → 301-redirected to https:// by the proxy → MIME probe found no audio body → 714.

The Task 2 code-quality reviewer flagged this exact scenario when the slice was being merged. Fix: honor X-Forwarded-Proto + X-Forwarded-Host before falling back to r.TLS + r.Host. URL the speaker fetches now matches the scheme/host the client used.

Secondary fixes

1. Sonos friendlyName too noisy in the picker. Devices showed up as Living Room - Sonos Play:1 Media Renderer - RINCON_5CAAFD79.... The room name is buried at the start of a long ID-laden string. UpnpDiscoveryController.displayName(...) now strips on the first - for Sonos-manufactured devices so the chip shows just Living Room / Kitchen / etc. Generic UPnP devices that append a (192.168.x.x) IP suffix also get that stripped. Manufacturer + model still render as the row subtitle, so the device-type info isn't lost. Operator confirmed: "naming worked much better and views as expected."

2. Loud diagnostic logs in selectUpnp. The original runCatching swallowed silent early-return cases (currentTrack == null, transportFor(...) == null) without any signal. Every path now Timber.ws on early-return AND adds Timber.i breadcrumbs through the success path (mint, SetAVTransportURI, Play, done). This made the root-cause findable in 30s of logcat scrolling.

3. Release Timber tree. Debug builds had DebugTree; release builds had nothing planted, so the diagnostic Timber.w / Timber.e calls were silently dropped in production. ReleaseTree now plants at WARN+ via android.util.Log.println with the canonical Minstrel tag. Keeps DEBUG/INFO chatter out of production logcat while letting operator-driven adb logcat sessions still see real failures.

Commits

  • 036da9de — UPnP: clean Sonos friendlyName for picker display
  • 96f12d6a — UPnP select: loud logs on every code path
  • a9edc125 — release-build Timber tree at WARN+ for operator diagnosis
  • 7d15f57e — cast token URL honors X-Forwarded-Proto / Host (the root-cause fix)

Test plan

  • Server container redeploys with the new image
  • Open NowPlaying sheet → Sonos rows show clean room names (already confirmed by operator)
  • Tap a Sonos device → audio plays through it (the v2026.06.03 hotfix delta)
  • Logcat shows the Minstrel/OutputPickerController breadcrumb sequence ending in "UPnP select: done"

🤖 Generated with Claude Code

Hotfix for v2026.06.03's UPnP path. On-device verification with a real Sonos speaker uncovered three issues; this PR ships all three plus the operator-diagnostic plumbing that made the root-cause findable. ## Root cause — UPnP error 714 from Sonos (PRIMARY FIX) On-device tap of a Sonos device dismissed the picker sheet but no audio routed. Logcat: ``` UPnP select: mint token for track=da85199c-..., route=Living Room POST /api/cast/stream-token → 200 (token minted) SetAVTransportURI to http://minstrel.fabledsword.com/api/tracks/.../stream?token=...&exp=... <-- 500 from Sonos SoapFaultException: SOAP fault 714 (IllegalMimeType) ``` UPnP error 714 = "Illegal MIME-Type" — Sonos does a HEAD/probe to detect the audio MIME at the URL. Our `cast_token.go` URL builder trusted `r.TLS` for the scheme, which is **nil behind a TLS-terminating reverse proxy**, so the URL came out `http://`. Sonos hit `http://` → 301-redirected to `https://` by the proxy → MIME probe found no audio body → 714. The Task 2 code-quality reviewer flagged this exact scenario when the slice was being merged. Fix: honor `X-Forwarded-Proto` + `X-Forwarded-Host` before falling back to `r.TLS` + `r.Host`. URL the speaker fetches now matches the scheme/host the client used. ## Secondary fixes **1. Sonos friendlyName too noisy in the picker.** Devices showed up as `Living Room - Sonos Play:1 Media Renderer - RINCON_5CAAFD79...`. The room name is buried at the start of a long ID-laden string. `UpnpDiscoveryController.displayName(...)` now strips on the first ` - ` for Sonos-manufactured devices so the chip shows just `Living Room` / `Kitchen` / etc. Generic UPnP devices that append a `(192.168.x.x)` IP suffix also get that stripped. Manufacturer + model still render as the row subtitle, so the device-type info isn't lost. **Operator confirmed:** "naming worked much better and views as expected." **2. Loud diagnostic logs in `selectUpnp`.** The original `runCatching` swallowed silent early-return cases (`currentTrack == null`, `transportFor(...) == null`) without any signal. Every path now `Timber.w`s on early-return AND adds `Timber.i` breadcrumbs through the success path (mint, SetAVTransportURI, Play, done). This made the root-cause findable in 30s of logcat scrolling. **3. Release Timber tree.** Debug builds had `DebugTree`; release builds had nothing planted, so the diagnostic Timber.w / Timber.e calls were silently dropped in production. `ReleaseTree` now plants at WARN+ via `android.util.Log.println` with the canonical `Minstrel` tag. Keeps DEBUG/INFO chatter out of production logcat while letting operator-driven `adb logcat` sessions still see real failures. ## Commits - `036da9de` — UPnP: clean Sonos friendlyName for picker display - `96f12d6a` — UPnP select: loud logs on every code path - `a9edc125` — release-build Timber tree at WARN+ for operator diagnosis - `7d15f57e` — cast token URL honors X-Forwarded-Proto / Host (the root-cause fix) ## Test plan - [ ] Server container redeploys with the new image - [ ] Open NowPlaying sheet → Sonos rows show clean room names (already confirmed by operator) - [ ] Tap a Sonos device → audio plays through it (the v2026.06.03 hotfix delta) - [ ] Logcat shows the `Minstrel/OutputPickerController` breadcrumb sequence ending in "UPnP select: done" 🤖 Generated with [Claude Code](https://claude.com/claude-code)
bvandeusen added 4 commits 2026-06-03 15:30:03 -04:00
fix(android): UPnP - clean Sonos friendlyName for picker display
android / Build + lint + test (push) Successful in 4m28s
036da9dea8
Sonos uses the friendlyName format
  'Room - Device Type - RINCON_<UDN>'

The picker was showing it verbatim, so the user saw rows like
  'Living Room - Sonos Play:1 Media Renderer - RINCON_5CAAFD79...'

Now strips on the first ' - ' for Sonos manufacturer matches, so the
chip shows just 'Living Room' / 'Kitchen' / etc. Subtitle (manufacturer
+ model) still renders below per the existing sheet design, so the
device-type info isn't lost.

Generic UPnP devices that append a '(192.168.x.x)' IP suffix get that
stripped too via an end-of-string-anchored regex. Empty / blank
friendlyName still falls back to 'Network speaker'.
fix(android): UPnP select - loud logs on every code path
android / Build + lint + test (push) Has been cancelled
96f12d6aac
Two silent early returns in selectUpnp were swallowing the most
likely failure modes:
  - currentTrack null (nothing playing locally → can't cast a track)
  - transportFor() returns null (route disappeared or id mismatch)

On-device verification reported 'tap collapses the sheet but no
audio routes', with logcat empty - one of these was firing without
any signal.

Each early-return now Timber.w's why; the runCatching block adds
Timber.i breadcrumbs at every step (mint token, SetAVTransportURI,
Play, done) so the next failure shows exactly how far we got.
fix(android): release-build Timber tree at WARN+ for operator diagnosis
android / Build + lint + test (push) Successful in 4m2s
a9edc12523
Debug builds got DebugTree; release builds had no tree planted at
all, so Timber.w / Timber.e calls were dropped silently in
production. That's how the UPnP select diagnostic-prints went
invisible during on-device testing - the released APK had no Timber
output reaching logcat.

Plant a release-only Tree that emits at WARN and above via
android.util.Log.println with the canonical 'Minstrel' tag (or the
caller-supplied tag when present). Keeps DEBUG / INFO traffic out of
production logcat (the chatty stuff is the part we don't want
flooding the buffer) while letting operator-driven adb logcat
sessions still see real failures.
fix(server): cast token URL honors X-Forwarded-Proto / Host
test-go / test (push) Successful in 36s
test-go / integration (push) Successful in 9m48s
7d15f57e86
On-device test against Sonos showed SetAVTransportURI returning UPnP
error 714 (IllegalMimeType). Logcat:

  POST /api/cast/stream-token -> 200 (token minted)
  SetAVTransportURI to http://minstrel.fabledsword.com/...
  <-- 500 from Sonos: SoapFaultException SOAP fault 714

The server is behind a TLS-terminating reverse proxy, so r.TLS is
nil and the URL builder emitted http://. Sonos does a HEAD probe to
detect the audio MIME type; against an http:// URL that 301s to
https://, the probe finds no audio body and bails with 714.

The Task 2 code-quality reviewer flagged this exact scenario at the
time. Closing it now: honor X-Forwarded-Proto + X-Forwarded-Host
before falling back to r.TLS + r.Host. Public URL the speaker
fetches now matches the scheme/host the client used to reach the
endpoint.
bvandeusen merged commit 3c4c27fb08 into main 2026-06-03 15:30:09 -04:00
Sign in to join this conversation.
No Reviewers
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: bvandeusen/minstrel#78