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
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)
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'.
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.
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.
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.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
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 error 714 = "Illegal MIME-Type" — Sonos does a HEAD/probe to detect the audio MIME at the URL. Our
cast_token.goURL builder trustedr.TLSfor the scheme, which is nil behind a TLS-terminating reverse proxy, so the URL came outhttp://. Sonos hithttp://→ 301-redirected tohttps://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-Hostbefore falling back tor.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 justLiving 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 originalrunCatchingswallowed silent early-return cases (currentTrack == null,transportFor(...) == null) without any signal. Every path nowTimber.ws on early-return AND addsTimber.ibreadcrumbs 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.ReleaseTreenow plants at WARN+ viaandroid.util.Log.printlnwith the canonicalMinstreltag. Keeps DEBUG/INFO chatter out of production logcat while letting operator-drivenadb logcatsessions still see real failures.Commits
036da9de— UPnP: clean Sonos friendlyName for picker display96f12d6a— UPnP select: loud logs on every code patha9edc125— release-build Timber tree at WARN+ for operator diagnosis7d15f57e— cast token URL honors X-Forwarded-Proto / Host (the root-cause fix)Test plan
Minstrel/OutputPickerControllerbreadcrumb sequence ending in "UPnP select: done"🤖 Generated with Claude Code