Sonos Radio Stream Checker
Work out why a radio stream URL fails on Sonos. Every redirect hop is checked against the real Sonos trust store and the TLS capabilities of S1 and S2 firmware, which differ.
Overview
Sonos players are not browsers. Their firmware ships a fixed trust store and a narrow set of TLS versions, ciphers and elliptic curves. A stream that plays perfectly in every browser can still be rejected by a speaker, and the speaker will not tell you why. This tool answers that question.
S1 and S2 are reported separately because their capabilities genuinely differ. A stream is frequently compatible with S2 and not S1, which is exactly the case that is hardest to diagnose by hand.
Redirects are followed and each hop is checked in its own right. That matters: a stream often starts on a modern host and redirects to an older CDN, and it is the last hop that decides whether playback works.
Web interface
Paste the stream URL and pick a region. The check then runs from an exit point in that country, which is the point of the selector: geo-restricted streams and geo-routed CDNs answer differently depending on where the request comes from, so checking a UK station from Germany can produce a misleading result.
Available regions are Germany (eu, the default), United States (us), United
Kingdom (gb) and France (fr). A banner reports whether that routing is currently
available.
What gets checked
Up to fifteen checks run per hop. Each returns its own verdict for S1 and S2. Two of them appear only when something is wrong, and are listed last.
| Check | What it tells you |
|---|---|
| Connection | Whether the host answers at all. |
| DNS Resolution | Whether the hostname resolves, and to what. |
| IP Address Check | Whether the resolved address is a routable public one. |
| Protocol | HTTP or HTTPS, and how the two behave across the chain. |
| TLS Version | Negotiated version against what each firmware accepts. |
| Cipher Suites | The usual culprit. Old players accept a short list. |
| Certificate Chain | Validity of every certificate presented, and whether the chain is complete. A server sending only its own certificate is missing its intermediate, which browsers hide and players do not. |
| Sonos Trust Status | Whether the root is in the Sonos trust bundle. A certificate every browser trusts can still fail here. |
| DNS Name Match | Whether the certificate actually covers the hostname. |
| Elliptic Curves | Curve support, the second most common silent failure. |
| Stream Metadata | Content type and codec of what is actually served. |
| M3U Playlist Detected | The URL is a playlist, not a stream. |
| PLS Playlist Detected | As above, other playlist format. |
| Security | Only on a refused hop. A redirect target or playlist entry pointing into a private or reserved network is reported rather than followed. |
| Playlist Nesting | Only when a playlist names another playlist. The second one is reported, not opened. |
The trust bundle is fetched from Sonos directly and cached for 30 days, so trust verdicts reflect what players actually ship rather than what the operating system trusts.
Sonos follows at most four redirect hops. A chain longer than that fails on the player
even when every individual hop is perfectly valid, so the response reports
too_many_hops separately from the compatibility verdicts.
API
/check/sonos/stream
JSON request body.
| Name | Type | Default | Description |
|---|---|---|---|
| url | string | required | The stream URL. http or https only, at most 2048 characters. |
| region | enum | eu |
eu, us, gb or fr. An unknown value
falls back to eu rather than failing. |
$ curl -s -X POST https://api.troubleshooting.tools/v1/check/sonos/stream \
-H "Content-Type: application/json" \
-d '{"url": "https://stream.example.com/radio.mp3", "region": "eu"}' | jq .
{
"url": "https://stream.example.com/radio.mp3",
"compatible_s1": false,
"compatible_s2": true,
"hop_count": 2,
"sonos_max_hops": 4,
"too_many_hops": false,
"m3u_detected": false,
"pls_detected": false,
"chain_analysis": [
{
"url": "https://stream.example.com/radio.mp3",
"http_code": 302,
"location": "https://cdn.example.net/radio.mp3",
"compatible_s1": true,
"compatible_s2": true,
"checks": [
{ "name": "TLS Version", "compatible_s1": true, "compatible_s2": true },
{ "name": "Cipher Suites", "compatible_s1": true, "compatible_s2": true }
],
"certificates": []
}
]
}
Response fields
| Field | Type | Description |
|---|---|---|
| compatible_s1 | boolean | Overall verdict for S1. False if any hop fails for S1. |
| compatible_s2 | boolean | Same for S2. |
| chain_analysis | array | One entry per redirect hop, in order. Each carries url, http_code, location, its own compatibility verdicts, the checks array and any certificates. |
| hop_count | integer | Number of hops in the chain. |
| sonos_max_hops | integer | What players allow. Currently 4. |
| too_many_hops | boolean | True when hop_count exceeds that. |
| m3u_detected | boolean | The URL is an M3U playlist. |
| m3u_recommendation | string | Only present when detected: the inner URL to check instead. |
| pls_detected | boolean | As above for PLS. |
| pls_recommendation | string | As above. |
| stream_check | object | Present on a hop that turned out to be a playlist. The stream URL named inside it, checked as a request of its own. Shaped like a hop, with redirect_chain, hop_count and too_many_hops of its own. |
Read the top-level verdicts first, then walk chain_analysis to find which hop and which check
caused a false. A playlist detection does not mean you are checking the wrong URL: the stream it names is
followed and checked for you, under stream_check.
Fields that appear only in certain situations
| Field | Where | Meaning |
|---|---|---|
| chain_complete | Certificate Chain check | False when the server sent only its own certificate and no intermediate. The hop is marked incompatible with both S1 and S2. |
| tls_diagnosis | Connection check, error_info | Why a handshake failed, asked of OpenSSL rather than guessed: verify_code, verify_text and certs_sent. PHP reports a failed verification without a reason, so this is where the reason lives. |
| redirect_chain | stream_check | The further hops the playlist's stream URL goes through, each with its own checks. Empty when it answers directly. |
| nested_playlist_not_followed | stream_check | True when the playlist named another playlist. It is reported and not opened, since such a chain has no end. |
A playlist entry is treated as a new request, not as further hops of the first one.
That is what happens on the player: it fetches the playlist, reads a URL out of the body and asks again,
so the stream URL starts with a full four hop budget of its own. Its hop_count is therefore
separate from the one at the top level.
Errors
Validation failures return {"error": "..."} at HTTP 200, not 4xx.
Test for the presence of an error key rather than branching on the status code.
| Message | Cause |
|---|---|
No URL provided | Missing url. |
Invalid URL format | Unparseable URL. |
Only HTTP and HTTPS protocols are allowed | Some other scheme. |
URLs pointing to private or reserved IP ranges are not allowed | Resolves to a non-public address. |
URLs pointing to localhost are not allowed | As above. |
Limits
A full check opens real TLS connections to every hop and can take several seconds. Allow a generous client timeout; 30 seconds is a sensible floor.
This endpoint has a limit of its own, tighter than the 50 requests per second the rest of the API
allows: 5 requests per second per IP with a burst of 5, and at most 2 checks at
once per IP. Over either, the answer is HTTP 429. A check holds a server worker for
its whole duration, which is what the concurrency limit protects. Batch station lists slowly and cache the
verdicts.
Privacy
Checked URLs appear in server logs for operational purposes and are not sold or shared. The request reaches the stream host from this service and, when a non-default region is selected, from an exit point in that country, not from your address.