Skip to content

fix(web-radio): try the round-robin host, and retry, before failing - #455

Merged
InstaZDLL merged 5 commits into
mainfrom
fix/452-web-radio-host-fallback
Jul 29, 2026
Merged

fix(web-radio): try the round-robin host, and retry, before failing#455
InstaZDLL merged 5 commits into
mainfrom
fix/452-web-radio-host-fallback

Conversation

@InstaZDLL

@InstaZDLL InstaZDLL commented Jul 28, 2026

Copy link
Copy Markdown
Owner

Fixes #452@jo-el414 reported that on Windows the first category click after reopening WaveFlow often failed with a server error, and that leaving the Web Radio page and coming back made it work.

Cause

The plugin pinned a single host with no fallback and no retry:

const MIRROR: &str = "https://de1.api.radio-browser.info";

Any hiccup on the first HTTP call of a session went straight to the user, and the "leave and come back" workaround was simply a second attempt. (list_entries is a static list, so the failure can only come from the first real request — which matches the report exactly.)

What I checked before writing the fix

The pin rested on the comment's own claim that "radio-browser uses round-robin DNS across regional mirrors (de1, at1, us1, …); the chosen prefix doesn't matter". That is no longer true:

host result
de1.api.radio-browser.info 200 in 0.37 s
nl1 / at1 / fi1 do not resolve at all
all.api.radio-browser.info 200 in 0.29 s

So the obvious fix — a hard-coded list of named nodes — would have rotted exactly like the current comment did.

Fix

all.api.radio-browser.info first: it's the project's official round-robin DNS entry, resolving only to nodes that are alive, so a node going down needs no edit here. de1 follows as a second attempt — both because it does answer, and because having a second entry is what makes a failed first request get retried at all, which is the whole user-visible bug.

A 4xx short-circuits instead of retrying: that's the API rejecting the request itself (bad tag, malformed query) and every node would answer the same.

Both hosts are already covered by the manifest allowlist (https://*.api.radio-browser.info/**) — no permission change.

Verification

  • cargo check --target wasm32-wasip2 and cargo clippy -- -D warnings on the plugin — clean
  • plugin_web_radio integration test passes against the rebuilt component (it really instantiates it through the wasmtime host)
  • the shipped binary genuinely contains the change: strings plugin.wasm shows both hosts and WaveFlow/Web-Radio/0.1.1

Plugin bumped to 0.1.1 (the version rides in the User-Agent radio-browser asks for) and plugin.wasm rebuilt in both places it lives — the bundled resource and the core test fixture.

Follow-up in this PR: the error message itself

@jo-el414 sent the screenshot after this PR was opened, and it confirms the diagnosis — the failing call is de1, and the wording is a transport failure, so no HTTP response ever came back:

plugin web-radio: plugin: http: error sending request for url
(https://de1.api.radio-browser.info/json/stations/bycountrycodeexact/IL?…)

But it also exposes a gap: that's reqwest::Error's Display, which stops at the outermost layer. DNS lookup, connection refused, timeout and TLS handshake all render identically, and the frontend shows the string verbatim — so the one question worth answering is the one the message drops, and the next report would have been just as blind.

Second commit walks source() and appends each distinct cause, skipping layers that merely restate their parent (reqwest and hyper both do that) and capping the depth, since source() is author-controlled. Four unit tests cover it: root cause preserved, lone error, restating layer, pathological chain.

Two things to know

  1. The .wasm diff is larger than this change. It was rebuilt with a current toolchain (142 210 → 130 644 bytes), so the binary delta mixes the code change with a compiler-version change. The source diff is the reviewable part.
  2. The exact cause is inferred, not confirmed. I couldn't read the error text in the screenshot. Between "node briefly unavailable" and "first TLS handshake slower than the 5 s connect timeout", the log would decide — the retry covers both, but if it still happens, the log is worth asking for.

Worth noting for later: plugin.wasm is committed but no CI job rebuilds it, so a source-only change to this plugin would silently ship the old binary. I rebuilt by hand here (rustup + cargo-component, both user-space, no sudo). A build job would make that safe by construction.

Summary by CodeRabbit

  • Améliorations
    • Amélioration de la récupération Web Radio avec une liste ordonnée de serveurs de secours et bascule automatique en cas d’échec.
    • Routage des requêtes plus robuste (meilleure gestion des réponses et tentative de plusieurs hôtes).
  • Corrections
    • Messages d’erreur plus informatifs, avec meilleure conservation de la cause d’origine et limitation des chaînes trop longues.
  • Mise à jour
    • Le plugin Web Radio passe en version 0.1.1.

Reported in #452: on Windows, the first category click after reopening
WaveFlow often failed with a server error, and simply leaving the Web
Radio page and coming back made it work.

The plugin pinned a single host, `de1.api.radio-browser.info`, with no
fallback and no retry — so any hiccup on the first HTTP call of a
session surfaced straight to the user, and the "leave and come back"
workaround was just a second attempt.

The pin rested on the comment's assumption that "any regional prefix
serves the same catalogue, so the chosen one doesn't matter". Checked
against reality: of the prefixes that comment names, `at1`, `nl1` and
`fi1` no longer resolve at all. A hard-coded node list rots on its own.

So the first host is now `all.api.radio-browser.info`, the project's
official round-robin DNS entry, which resolves only to nodes that are
alive — no list to maintain. `de1` follows as a second attempt, both
because it does answer and because having a second entry means a failed
first request is retried at all, which is what the report comes down to.
A 4xx short-circuits: that's the API rejecting the request itself, and
every node would say the same.

Both hosts are already covered by the manifest allowlist
(`https://*.api.radio-browser.info/**`), so permissions are unchanged.

Plugin bumped to 0.1.1 (the version rides in the User-Agent
radio-browser asks for) and plugin.wasm rebuilt in both places it lives:
the bundled resource and the core test fixture.

Refs #452
@InstaZDLL InstaZDLL added scope: backend Rust/Tauri backend (src-tauri/) scope: plugins Plugin runtime, SDK, store, and bundled plugins type: fix Bug fix size: m 50-200 lines labels Jul 28, 2026
@coderabbitai

coderabbitai Bot commented Jul 28, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 17bf6f4b-b8e4-4970-addc-317575edaa86

📥 Commits

Reviewing files that changed from the base of the PR and between 9c2a55f and 5a7bff7.

📒 Files selected for processing (1)
  • src-tauri/crates/core/src/plugin/host_impl.rs

📝 Walkthrough

Walkthrough

Le plugin Web Radio remplace son miroir unique par plusieurs hosts de secours et construit des chemins d’API relatifs. Le runtime conserve les causes des erreurs réseau avec une profondeur limitée. Les manifests et la version Cargo passent de 0.1.0 à 0.1.1.

Changes

Résilience des requêtes réseau

Layer / File(s) Summary
Construction des chemins et repli entre hosts
src-tauri/plugins/web-radio/src/lib.rs
resolve transmet des chemins relatifs à fetch_json, qui essaie les hosts dans l’ordre, retourne les réponses 2xx, arrête sur les 4xx sauf 429 et poursuit après les erreurs 5xx ou réseau.
Description des erreurs HTTP
src-tauri/crates/core/src/plugin/host_impl.rs
describe_error conserve les causes racines, ignore les répétitions immédiates et borne les chaînes d’erreur utilisées pour les échecs d’envoi ou de lecture. Les tests couvrent ces comportements.
Synchronisation de la version du plugin
src-tauri/plugins/web-radio/Cargo.toml, src-tauri/plugins/web-radio/manifest.toml, src-tauri/resources/plugins/web-radio/manifest.toml, src-tauri/crates/core/tests/fixtures/web-radio/manifest.toml
La version du paquet, des manifests et de la fixture passe de 0.1.0 à 0.1.1.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Resolve
  participant FetchJson
  participant Hosts
  participant RadioAPI
  Resolve->>FetchJson: transmettre le chemin relatif
  FetchJson->>Hosts: parcourir les hosts configurés
  FetchJson->>RadioAPI: envoyer GET host + path
  RadioAPI-->>FetchJson: retourner le statut et le corps
  FetchJson-->>Resolve: retourner le corps sur réponse 2xx
Loading

Possibly related PRs

  • InstaZDLL/WaveFlow#222 — Modifie également la conversion et la propagation des erreurs HTTP dans waveflow:host/http.send.
  • InstaZDLL/WaveFlow#226 — Introduit l’implémentation Web Radio sur laquelle s’appuie la refonte du routage dans src-tauri/plugins/web-radio/src/lib.rs.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed Le titre résume bien le changement principal : ajout de fallback/réessai sur les hôtes Web Radio.
Description check ✅ Passed La description couvre le bug, la cause, la correction et la vérification; elle est suffisamment complète malgré le gabarit non strictement suivi.
Linked Issues check ✅ Passed La PR répond au bug #452 en ajoutant un fallback et des réessais pour les requêtes Web Radio initiales.
Out of Scope Changes check ✅ Passed Les changements annexes (version, manifeste, reconstruction wasm, tests) restent liés au correctif et ne semblent pas hors périmètre.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing Touches
📝 Generate docstrings
  • ✅ Generated successfully - (🔄 Check to regenerate)
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/452-web-radio-host-fallback

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src-tauri/plugins/web-radio/src/lib.rs (1)

148-185: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Suffixe de requête dupliqué trois fois.

limit={PAGE_LIMIT}&order=votes&reverse=true&hidebroken=true est répété à l'identique pour tag, country et search. Extraire une constante partagée limiterait le risque de divergence si un paramètre change un jour.

♻️ Exemple de factorisation
+const SORT_SUFFIX: &str = "order=votes&reverse=true&hidebroken=true";
+
 fn build_path(query: &str) -> Result<String, String> {
     ...
         return Ok(format!(
-            "/json/stations/bytag/{tag}?limit={PAGE_LIMIT}&order=votes&reverse=true&hidebroken=true"
+            "/json/stations/bytag/{tag}?limit={PAGE_LIMIT}&{SORT_SUFFIX}"
         ));
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src-tauri/plugins/web-radio/src/lib.rs` around lines 148 - 185, Extract the
repeated query suffix used by the tag, country, and search branches into one
shared constant near the request-building logic, then interpolate that constant
in each endpoint format. Preserve the existing parameter values and
endpoint-specific path/query keys while ensuring all three branches reuse the
same suffix.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src-tauri/plugins/web-radio/src/lib.rs`:
- Around line 188-236: Update the status handling in fetch_json so fail-fast
applies only to 4xx responses other than 429; let 429 continue through the retry
path like 5xx responses. Preserve successful 2xx handling and retry behavior for
transport errors and other statuses.

---

Outside diff comments:
In `@src-tauri/plugins/web-radio/src/lib.rs`:
- Around line 148-185: Extract the repeated query suffix used by the tag,
country, and search branches into one shared constant near the request-building
logic, then interpolate that constant in each endpoint format. Preserve the
existing parameter values and endpoint-specific path/query keys while ensuring
all three branches reuse the same suffix.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 201f764a-8045-4944-b40b-4a8fec359357

📥 Commits

Reviewing files that changed from the base of the PR and between 0ef99f1 and 4a20483.

⛔ Files ignored due to path filters (3)
  • src-tauri/crates/core/tests/fixtures/web-radio/plugin.wasm is excluded by !**/*.wasm
  • src-tauri/plugins/web-radio/Cargo.lock is excluded by !**/*.lock
  • src-tauri/resources/plugins/web-radio/plugin.wasm is excluded by !**/*.wasm
📒 Files selected for processing (5)
  • src-tauri/crates/core/tests/fixtures/web-radio/manifest.toml
  • src-tauri/plugins/web-radio/Cargo.toml
  • src-tauri/plugins/web-radio/manifest.toml
  • src-tauri/plugins/web-radio/src/lib.rs
  • src-tauri/resources/plugins/web-radio/manifest.toml

Comment thread src-tauri/plugins/web-radio/src/lib.rs
The report on #452 came with this, and nothing more:

    plugin web-radio: plugin: http: error sending request for url
    (https://de1.api.radio-browser.info/json/stations/bycountrycodeexact/IL?…)

That is `reqwest::Error`'s Display, which stops at the outermost
layer. It says the transport failed but never says why — DNS lookup,
connection refused, timeout and TLS handshake all render identically,
and the frontend shows this string verbatim. So the one question worth
answering ("which of those was it?") is exactly the one the message
drops, and the next report would have been just as blind.

Walk `source()` and append each distinct cause. Layers that restate
their parent are skipped (reqwest and hyper both do this, and "timed
out: timed out" reads worse than the line it repeats), and the walk is
depth-capped because `source()` is author-controlled.

Refs #452
@InstaZDLL InstaZDLL added size: l 200-500 lines and removed size: m 50-200 lines labels Jul 29, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src-tauri/crates/core/src/plugin/host_impl.rs`:
- Around line 347-370: Update describe_error so deduplication compares each
cause message only with the immediately preceding error-layer message, not the
accumulated out string. Track the last individual message in a separate
variable, update it for every traversed cause, and append the current cause
unless it exactly repeats that parent message; preserve the existing depth limit
and output formatting.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: c275ea89-3835-46f2-9c07-653d9ed3d6d3

📥 Commits

Reviewing files that changed from the base of the PR and between 4a20483 and 8546324.

📒 Files selected for processing (1)
  • src-tauri/crates/core/src/plugin/host_impl.rs

Comment thread src-tauri/crates/core/src/plugin/host_impl.rs
Review catch on #455. `resp.status < 500` fails fast on 429 Too Many
Requests, but a rate limit is per-node state rather than a verdict on
the request — routing around a node that won't serve us right now is
exactly what the second host is for. The same condition also swallowed
3xx (the host client disables redirects, so those arrive verbatim),
which are likewise not a rejection of the request.

Narrow the fail-fast to genuine 4xx minus 429; everything else moves on
to the next host.

Refs #452
`order=votes&reverse=true&hidebroken=true` was spelled out identically
in the tag, country and search branches. The three category kinds have
to rank the same way, so a future tweak would have had to be remembered
in three places.

`topvote` / `lastchange` deliberately stay out of it: they carry their
ordering in the endpoint name and take the limit as a path segment.

Outside-diff review note on #455.

Refs #452
@InstaZDLL

Copy link
Copy Markdown
Owner Author

@coderabbitai La remarque hors-diff sur le suffixe dupliqué m'avait échappé — traitée en 9c2a55f.

Extrait en LIST_PARAMS, avec deux écarts assumés par rapport à la suggestion :

  • nommé LIST_PARAMS plutôt que SORT_SUFFIX : le groupe contient hidebroken=true, qui est un filtre et non un critère de tri ;
  • topvote / lastchange volontairement exclus : ils portent leur ordre dans le nom de l'endpoint et prennent la limite comme segment de chemin, donc les inclure aurait forcé une abstraction là où il n'y a pas de forme commune.

La vraie raison de factoriser ici n'est pas la répétition en soi : les trois catégories doivent classer de façon identique, donc une modification future (un autre order, par exemple) devait être répercutée à trois endroits sans que rien ne le signale.

.wasm reconstruit et recopié aux deux emplacements ; strings sur le binaire livré ne montre plus qu'une occurrence du suffixe au lieu de trois. Clippy et plugin_web_radio verts.

@coderabbitai

coderabbitai Bot commented Jul 29, 2026

Copy link
Copy Markdown

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

git show --stat --oneline --decorate --no-renames 9c2a55f
git show --format=fuller --no-ext-diff --unified=40 9c2a55f -- src-tauri/plugins/web-radio/src/lib.rs

ast-grep outline src-tauri/plugins/web-radio/src/lib.rs --items all --view expanded
rg -n -C 3 'LIST_PARAMS|hidebroken=true|order=|topvote|lastchange' src-tauri/plugins/web-radio/src/lib.rs

for wasm in \
  src-tauri/resources/plugins/web-radio/plugin.wasm \
  src-tauri/crates/core/tests/fixtures/web-radio/plugin.wasm
do
  if [ -f "$wasm" ]; then
    printf '\n== %s ==\n' "$wasm"
    strings "$wasm" | rg -n 'hidebroken=true|order=|topvote|lastchange' || true
  else
    printf '\nMISSING: %s\n' "$wasm"
  fi
done

Length of output: 15214


🏁 Script executed:

#!/bin/bash
set -euo pipefail

needle='order=votes&reverse=true&hidebroken=true'

for wasm in \
  src-tauri/resources/plugins/web-radio/plugin.wasm \
  src-tauri/crates/core/tests/fixtures/web-radio/plugin.wasm
do
  printf '\n== %s ==\n' "$wasm"
  count=$(LC_ALL=C grep -aFo "$needle" "$wasm" | wc -l | tr -d ' ')
  printf 'shared-suffix occurrences: %s\n' "$count"
  LC_ALL=C grep -aobF "$needle" "$wasm" || true
done

Length of output: 832


@InstaZDLL Bien vu. Vérifié dans 9c2a55f : LIST_PARAMS centralise bien les trois endpoints qui doivent rester cohérents, tandis que topvote et lastchange conservent à juste titre leur format spécifique.

Les deux plugin.wasm contiennent chacun exactement une occurrence du suffixe partagé. Remarque traitée. ✅

Review catch on #455. `out.ends_with(&text)` asked "does this cause end
the string built so far", not "does it repeat the layer above it". A
distinct cause that happens to be a suffix of the accumulated text was
therefore dropped: "failed to connect" followed by its own "connect"
layer collapsed to just the first, losing information the whole
function exists to preserve.

Track the last appended message separately and require an exact match.
The new test fails against the previous logic, which is the point.

Refs #452
@coderabbitai

coderabbitai Bot commented Jul 29, 2026

Copy link
Copy Markdown

Note

Docstrings generation - SKIPPED
Skipped regeneration as there are no new commits. Docstrings already generated for this pull request at #456.

coderabbitai Bot added a commit that referenced this pull request Jul 29, 2026
Docstrings generation was requested by @InstaZDLL.

* #455 (comment)

The following files were modified:

* `src-tauri/crates/core/src/plugin/host_impl.rs`
* `src-tauri/plugins/web-radio/src/lib.rs`
@InstaZDLL
InstaZDLL merged commit d35d640 into main Jul 29, 2026
14 checks passed
@InstaZDLL
InstaZDLL deleted the fix/452-web-radio-host-fallback branch July 29, 2026 16:38
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

scope: backend Rust/Tauri backend (src-tauri/) scope: plugins Plugin runtime, SDK, store, and bundled plugins size: l 200-500 lines type: fix Bug fix

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Some info for v.1.7.0 + bug: webradio some times conect to server error.

1 participant