Skip to content

fix(subsonic): an unsaved play queue sends no element at all - #259

Merged
InstaZDLL merged 1 commit into
mainfrom
fix/empty-play-queue-omits-the-element
Oct 12, 2026
Merged

InstaZDLL merged 1 commit into
mainfrom
fix/empty-play-queue-omits-the-element

Conversation

@InstaZDLL

@InstaZDLL InstaZDLL commented Oct 12, 2026 •

Copy link
Copy Markdown
Owner

getPlayQueue answered a bare <playQueue/> before anything had been saved.
The 1.16.1 schema marks username, changed and changedBy all three
use="required" on PlayQueue, so that element has no valid form for a
queue that was never written — and the populated answer, which carries all
three since #225, disagreed with it as well as with the schema.

What it answers now

<subsonic-response status="ok" version="1.16.1" …/>

Which is what the reference documents: "or an empty <subsonic-response> if
no play queue has been saved"
. <xs:choice minOccurs="0"> on Response makes
the omission valid; no non-empty shape is.

Why not fill it in

That was the other way out, and it is the worse one: changed and changedBy
would have to be invented for something that was never changed.

Navidrome takes a third route — it emits the element carrying username
alone — which is equally invalid against the schema and only exchanges one
deviation for another. It is worth naming because it sets the client risk: a
client that copes with Navidrome's shape copes with omission too, since
omission is what Subsonic and Airsonic answer. One that could not read an
absent playQueue would already fail against both.

How it is done

Node::nothing() is the per-answer half of empty_success_method, which
is per method: some methods have nothing to say only sometimes. An element name
can never be empty, so the empty name is a sentinel no real node collides with.

The pinned assertion

play_queue_and_now_playing_name_their_children_entry pinned the old shape on
purpose, so that it could not change unnoticed. It is rewritten rather than
deleted, and now asserts the envelope beside the absence — an error
response carries no playQueue either, and absence alone cannot tell a
conforming answer from a broken one.

Both halves were proved by inversion, each on its own: with the fix
reverted, the JSON assertion fails on {"playQueue":{}} and the XML assertion,
run with the JSON one neutralised, fails on <playQueue/>. The XML half is not
decoration — an element present but empty reads as absent to a JSON-only test.

subsonic_xml joins subsonic_json in tests/support/, and the populated
half of the same test now goes through it instead of building its own request.

Documentation

  • RFC-002 — a fourth accepted deviation from the v2.0-beta freeze, with the
    Navidrome comparison. Like the other three, it requires the compatibility
    matrix to be re-run before the next tag.
  • docs/subsonic-compatibility.md — a note saying the rows below were right
    when written, and naming what to check: DSub and Juliet are the two clients
    in the matrix that call the method.
  • docs/subsonic-api-guide.md — the queue row says what an unsaved queue
    answers.

No CHANGELOG.md entry: that file is written at release time, last touched by
the 2.0.0-beta.1 release PR.

Closes #229

Summary by CodeRabbit

  • Corrections
    • Lorsqu’aucune file de lecture n’a été enregistrée, getPlayQueue renvoie désormais une réponse réussie sans élément playQueue, en XML comme en JSON. Les réponses incluent toujours les informations de la file lorsqu’elle existe.
  • Documentation
    • La documentation précise le comportement de getPlayQueue sans file enregistrée et signale que la compatibilité avec DSub et Juliet reste à vérifier.

The schema marks `username`, `changed` and `changedBy` all three required on
`PlayQueue`, so the bare `<playQueue/>` this answered before anything was
saved had no valid form. The reference documents the omission rather than a
shape — "an empty <subsonic-response> if no play queue has been saved" — and
filling the element in would mean inventing a `changed` and a `changedBy` for
something that was never changed.

Navidrome takes a third route and sends the element with `username` alone,
which is equally invalid and only exchanges one deviation for another. A client
that copes with that copes with omission too: omission is what Subsonic and
Airsonic answer, so a client that could not read it would already fail there.

`Node::nothing()` is the per-answer half of `empty_success_method`, which is
per method. An element name can never be empty, so the empty name is a sentinel
no real node collides with.

The pinned assertion is rewritten rather than deleted, and asserts the envelope
beside the absence: an error response carries no `playQueue` either, and
absence alone cannot tell a conforming answer from a broken one. Both halves
were proved by inversion, each on its own.

Closes #229

Signed-off-by: InstaZDLL <github.105mh@8shield.net>
@github-actions github-actions Bot added scope: server Server core (Rust) scope: docs Docs, README, assets scope: subsonic Subsonic / OpenSubsonic compatibility type: fix Bug fix size: m 50-200 lines labels Oct 12, 2026
@coderabbitai

coderabbitai Bot commented Oct 12, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration
  • Configuration used: Repository: InstaZDLL/waveflow-server/.coderabbit.yaml
  • Review profile: ASSERTIVE
  • Plan: Advanced
  • Run ID: 64f57e45-60b3-4c9b-a031-e2b142bdb711

📥 Commits

Reviewing files that changed from the base of the PR and between bba2907 and f00e400.


📒 Files selected for processing (7)
  • docs/rfcs/RFC-002-waveflow-server-v2.md
  • docs/subsonic-api-guide.md
  • docs/subsonic-compatibility.md
  • src/subsonic/mod.rs
  • src/subsonic/userdata.rs
  • tests/subsonic_contract.rs
  • tests/support/mod.rs

Included review availability: This review used your included allowance. 0 included reviews remain after this review. Your included PR review attempts over the past 7 days set your current allowance at 2 reviews per hour.



📝 Walkthrough

Walkthrough

Quand aucune file n’est enregistrée, getPlayQueue renvoie une enveloppe de succès sans élément playQueue. Les tests couvrent cette réponse en XML et en JSON. La documentation décrit ce comportement comme une quatrième déviation au contrat Subsonic v2.0-beta.

Changes

Réponse de getPlayQueue

Étape / fichier(s) Résumé
Contrat et réponse sans file
docs/rfcs/RFC-002-waveflow-server-v2.md, docs/subsonic-api-guide.md, docs/subsonic-compatibility.md, src/subsonic/mod.rs, src/subsonic/userdata.rs
La documentation précise que playQueue est absent lorsqu’aucune file n’est enregistrée. get_queue renvoie Node::nothing() dans ce cas. La construction de la réponse omet le payload marqué comme vide.
Vérification XML et JSON
tests/support/mod.rs, tests/subsonic_contract.rs
Le test vérifie que les réponses XML et JSON ne contiennent pas playQueue pour une file non enregistrée. Le helper subsonic_xml envoie la requête et renvoie le corps de la réponse.

Priority: ⬇️ Low

Estimated code review effort: 2 (Simple) | ~10 minutes

Change: Bug fix · Severity of issue fixed: Low


Merge Risk | ⚪ Minimal · up to f00e4

Merge Risk: ⚪ Minimal · up to f00e4

The intentional empty-queue response is covered in JSON and XML, with no concrete client break established. DSub and Juliet remain to be checked in the planned compatibility campaign; no merge blocker is shown.

Security Architecture Review

Security architecture risk: 🔵 Low · up to f00e4

The response change is narrow and leaves authentication, account scoping, and saved-queue behavior unchanged. Compatibility with existing clients on fresh accounts remains unverified.

Retained concerns
No architecture-level concerns identified.

Security review details

Security Blast Radius

  • inferred — The changed observable behavior is confined to an authenticated caller reading an account-scoped unsaved queue. The inspected change adds no caller-selected account identity, privileged operation, or downstream authority.

Trust Boundaries and Controls

  • observed — The omission condition is evaluated only after successful authentication and successful dispatch. Authentication or queue-service failures cannot become successful empty responses through this new condition; they remain on the existing error-rendering path.

Resilience and Maintainability Implications

  • inferred — The changed branch constructs a response without writing queue state or adding a multi-step transition. It introduces no new partial-write, cleanup, or recovery state; reverting the response change would not require converting saved queues.

Pre-merge checks | Passed 3 | Failed 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Description check Warning La description explique le comportement, la justification, les tests ciblés et la documentation modifiée. Elle ne suit toutefois pas la structure requise et ne fournit pas le plan de test avec les cas… Ajouter les sections # Summary, ## Changes, ## Test plan et ## Notes. Détailler les changements sous forme de liste. Indiquer l’état des commandes requises (bun run typecheck, bun run lint, bun run format:check, `bun run build…
Docstring Coverage Warning Docstring coverage is 30.00% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 10 functions across 4 files. (3 skipped: … Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check Passed Le titre décrit clairement le changement principal : getPlayQueue n’envoie plus d’élément pour une file non enregistrée.
Linked Issues check Passed L’issue active directement liée #229 demande l’omission de playQueue avant toute sauvegarde et la mise à jour du test contractuel. get_queue renvoie Node::nothing() dans ce cas. handle_inner p…
Out of Scope Changes check Passed Les changements restent liés à #229. Le mécanisme Node::nothing() permet l’omission par réponse. Le helper subsonic_xml et les assertions JSON/XML valident le changement de format. RFC-002 et les …

Full details: Description check

Explanation

La description explique le comportement, la justification, les tests ciblés et la documentation modifiée. Elle ne suit toutefois pas la structure requise et ne fournit pas le plan de test avec les cases prévues.

Resolution

Ajouter les sections # Summary, ## Changes, ## Test plan et ## Notes. Détailler les changements sous forme de liste. Indiquer l’état des commandes requises (bun run typecheck, bun run lint, bun run format:check, bun run build, bun run test) et le test manuel, ou supprimer la section Notes si elle est inutile. Conserver la confirmation AGPL-3.0-only et DCO du modèle de dépôt, si elle est requise par le processus.


Full details: Docstring Coverage

Explanation

Docstring coverage is 30.00% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 10 functions across 4 files. (3 skipped: 3 unsupported.)


  • Fix all pre-merge checks with AI
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Commit to this branch
  • Create a new PR

🧪 Generate unit tests (beta)
  • Commit to this branch
  • Create a new PR

  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

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

@github-actions github-actions Bot added type: fix Bug fix and removed type: fix Bug fix labels Oct 12, 2026
@InstaZDLL InstaZDLL self-assigned this Oct 12, 2026
@InstaZDLL
InstaZDLL merged commit 7660e64 into main Oct 12, 2026
15 checks passed
@InstaZDLL
InstaZDLL deleted the fix/empty-play-queue-omits-the-element branch October 12, 2026 18:34
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

scope: docs Docs, README, assets scope: server Server core (Rust) scope: subsonic Subsonic / OpenSubsonic compatibility size: m 50-200 lines type: fix Bug fix

Projects

None yet

Development

Successfully merging this pull request may close these issues.

An empty getPlayQueue answers a container the schema has no valid form for

1 participant