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.

CheckWhat it tells you
ConnectionWhether the host answers at all.
DNS ResolutionWhether the hostname resolves, and to what.
IP Address CheckWhether the resolved address is a routable public one.
ProtocolHTTP or HTTPS, and how the two behave across the chain.
TLS VersionNegotiated version against what each firmware accepts.
Cipher SuitesThe usual culprit. Old players accept a short list.
Certificate ChainValidity 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 StatusWhether the root is in the Sonos trust bundle. A certificate every browser trusts can still fail here.
DNS Name MatchWhether the certificate actually covers the hostname.
Elliptic CurvesCurve support, the second most common silent failure.
Stream MetadataContent type and codec of what is actually served.
M3U Playlist DetectedThe URL is a playlist, not a stream.
PLS Playlist DetectedAs above, other playlist format.
SecurityOnly on a refused hop. A redirect target or playlist entry pointing into a private or reserved network is reported rather than followed.
Playlist NestingOnly 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

POST /check/sonos/stream

JSON request body.

NameTypeDefaultDescription
urlstringrequired The stream URL. http or https only, at most 2048 characters.
regionenumeu eu, us, gb or fr. An unknown value falls back to eu rather than failing.
shell
$ 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 .
json
{
  "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

FieldTypeDescription
compatible_s1booleanOverall verdict for S1. False if any hop fails for S1.
compatible_s2booleanSame for S2.
chain_analysisarrayOne entry per redirect hop, in order. Each carries url, http_code, location, its own compatibility verdicts, the checks array and any certificates.
hop_countintegerNumber of hops in the chain.
sonos_max_hopsintegerWhat players allow. Currently 4.
too_many_hopsbooleanTrue when hop_count exceeds that.
m3u_detectedbooleanThe URL is an M3U playlist.
m3u_recommendationstringOnly present when detected: the inner URL to check instead.
pls_detectedbooleanAs above for PLS.
pls_recommendationstringAs above.
stream_checkobjectPresent 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

FieldWhereMeaning
chain_completeCertificate Chain checkFalse when the server sent only its own certificate and no intermediate. The hop is marked incompatible with both S1 and S2.
tls_diagnosisConnection check, error_infoWhy 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_chainstream_checkThe further hops the playlist's stream URL goes through, each with its own checks. Empty when it answers directly.
nested_playlist_not_followedstream_checkTrue 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.

MessageCause
No URL providedMissing url.
Invalid URL formatUnparseable URL.
Only HTTP and HTTPS protocols are allowedSome other scheme.
URLs pointing to private or reserved IP ranges are not allowedResolves to a non-public address.
URLs pointing to localhost are not allowedAs 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.