Skip to content

fix(settings): fall back to the profile endpoint when the settings API is absent - #153

Closed
RXWatcher wants to merge 1 commit into
Silo-Server:mainfrom
RXWatcher:fix/settings-legacy-fallback-standalone
Closed

fix(settings): fall back to the profile endpoint when the settings API is absent#153
RXWatcher wants to merge 1 commit into
Silo-Server:mainfrom
RXWatcher:fix/settings-legacy-fallback-standalone

Conversation

@RXWatcher

Copy link
Copy Markdown
Contributor

Every profile setting is silently discarded on servers without the settings API

Reported as "I can't set Dutch as my default subtitle language — it snaps back to Off." It turned out to have nothing to do with subtitles, or with which languages the picker offers. Captured from a production server's own logs:

PUT /api/v1/settings/values/playback.subtitle_language  ->  404
GET /api/v1/settings/values/effective                   ->  404
GET /api/v1/settings/contract/capabilities              ->  404
GET /api/v1/settings/overlay-config                     ->  200   (older routes fine)

The server predates the canonical settings API and answers 404 to the whole family. The settings screen applies the change optimistically, the write 404s, and the ViewModel rolls back — so the row snaps straight back to its previous value with no error shown.

The diagnostic that isolates it: does English fail too? It does. That separates a picker problem from a write-path problem in one step.

Scope

Not subtitles — everything ProfileSettingsController owns:

Setting Behaviour before this PR
playback.subtitle_language silently reverted
playback.subtitle_mode silently reverted
playback.show_forced_subtitles silently reverted
catalog.metadata_language silently reverted

Subtitles are simply where a user notices first.

Availability.SERVER_UPGRADE_REQUIRED already exists for exactly this case — the screen is supposed to explain, "instead of rendering rows whose edits would silently go nowhere." The probe fires and returns that value, but the setters never consulted it, so the rows rendered as editable regardless.

The fix

These preferences rode PUT /profiles/{id} as named columns before the contract landed, and the class doc records the assumption that replaced it:

Android no longer depends on the profile endpoint accepting subtitle_language, subtitle_mode, show_forced_subtitles or preferred_metadata_language

That does not hold for servers without the contract. Those columns still work there, and the server mirrors them into canonical rows once upgraded, so the fallback loses nothing.

Writes — the canonical path is tried whenever the contract is believed present, so a modern server is never pushed onto legacy columns. Only a 404 triggers the fallback, and it flips the belief so subsequent writes skip the dead route rather than re-probing per keystroke. Any other failure is reported as a failure: retrying a rejected value on the legacy path would store precisely what the contract refused.

Reads — this mattered more than it first appears. load() returned a null snapshot on these servers, so the screen rendered defaults, and the next edit would write those defaults over preferences the viewer had never touched. It now reads the profile's real values.

Verification

Confirmed end-to-end against the same production server that produced the 404s, from a device build:

PUT /api/v1/profiles/29e8bb8f-…   status=200

The UI renders the profile returned by that PUT (no re-read, by design), so the value persisting is the confirmation the response carried it.

Seven unit tests, covering the two ways this is easy to get wrong:

  • an unprobed server still tries the contract first — assuming the worst would push every modern server onto legacy columns
  • a failing legacy write is reported, not claimed as success

plus: the contract is abandoned once rather than re-probed per write; the load returns real values instead of defaults; all four settings fall back, not just subtitle language; and with no fallback configured the failure still surfaces.

./gradlew test green.

Notes for review

  • ProfileSettingsController's companion moves from private to internal so the legacy implementation can share normalizeSubtitleMode. Same normalisation on both paths, rather than a second copy that drifts.
  • The fallback is constructor-injected and nullable; passing null disables it, which is what the existing canonical-path tests want.
  • Independent of feat(settings): offer every language the web does, and make them match #152 (the language-list PR) — they touch different problems and can land in either order.

…I is absent

Every profile setting is silently discarded on servers that predate the
canonical settings API. Confirmed against production:

  PUT /api/v1/settings/values/playback.subtitle_language  -> 404
  GET /api/v1/settings/values/effective                   -> 404
  GET /api/v1/settings/contract/capabilities              -> 404
  GET /api/v1/settings/overlay-config                     -> 200

The screen applies the change optimistically, the write 404s, and the
ViewModel rolls back — so the row snaps straight back to its old value with
no error shown. It affects subtitle language, subtitle mode, forced
subtitles and metadata language equally; subtitles are just where it gets
noticed. The controller's own doc asserted Android "no longer depends on the
profile endpoint accepting subtitle_language", and that assumption does not
hold for these servers.

The legacy columns on PUT /profiles/{id} still carry all four preferences
there, and the server mirrors them into canonical rows once upgraded, so the
fallback loses nothing.

- Reads: load() returns the profile's real values instead of a null snapshot.
  That mattered more than it looks — a null snapshot left the screen on
  defaults, and the next edit wrote those defaults over preferences the
  viewer had never touched.
- Writes: the canonical path is tried whenever the contract is believed
  present, so a modern server is never pushed onto the legacy columns. Only
  a 404 triggers the fallback, and it flips the belief so later writes skip
  the dead route. Any other failure is reported as a failure — retrying a
  rejected value on the legacy path would store what the contract refused.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jul 30, 2026

Copy link
Copy Markdown

Warning

Review limit reached

@RXWatcher, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 57 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: eb34056a-0e33-4bb2-ad62-0634aeb80745

📥 Commits

Reviewing files that changed from the base of the PR and between 83e23da and 1d49122.

📒 Files selected for processing (4)
  • shared/src/commonMain/kotlin/org/siloserver/silo/di/RepositoryModule.kt
  • shared/src/commonMain/kotlin/org/siloserver/silo/domain/settings/LegacyProfileSettings.kt
  • shared/src/commonMain/kotlin/org/siloserver/silo/domain/settings/ProfileSettingsController.kt
  • shared/src/commonTest/kotlin/org/siloserver/silo/domain/settings/LegacyProfileSettingsFallbackTest.kt

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@RXWatcher

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 30, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@Quick104

Copy link
Copy Markdown
Contributor

Closing because supported clients require the canonical settings API and server upgrades are mandatory. We will keep the canonical-only write path and fix the unsupported-server UX instead: show the existing upgrade-required notice, prevent edits to unavailable profile settings, and leave device-local subtitle controls usable.

@Quick104 Quick104 closed this Jul 31, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants