mirror of
https://github.com/Psychotoxical/psysonic.git
synced 2026-07-22 15:25:46 +00:00
96530f244e
* fix(servers): probe gated servers over native reqwest so custom headers connect Adding a server behind a header gate (Cloudflare Access, Pangolin service tokens) failed at the connect step (#1216). The connect probe ran in the WebView (axios); a non-safelisted header such as Authorization / CF-Access-* makes the browser send a CORS preflight OPTIONS first, and that preflight carries no token — so the gate rejects it and the real request never leaves. Streaming and covers already worked because they go through Rust (reqwest), which never preflights. Route the header-bearing connect probe through a new Tauri command `probe_server_connection` that runs the Subsonic ping over native reqwest with the per-server header context (endpoints + apply rule), reusing the same resolver as the data plane. Header-less servers keep the lightweight WebView path unchanged. - Rust: add `probe_server_connection` (+ `ServerProbeResult`) in app_api/network, register in collect_commands!/generate_handler!, regenerate specta bindings. - Frontend: `pingWithCredentialsForProfile` calls the command when the profile has custom headers; add `serverHttpContextWireForProbe` helper. - Tests: SubsonicClient sends the gate header on ping via http_context (and misses the gated matcher without it); frontend probe routing + WebView fallback; result mapping unit tests. * docs(changelog): note gated-server connect fix (#1272) * fix(servers): keep add form open and surface the reason when connect fails The add-server flow closed the form the instant you pressed Add and, on failure, left only a small status dot — so a bad password, a rejected gate header, or an unreachable host all looked the same ("nothing happened"). - The connect probe now returns a short failure reason (the server's own error message, an HTTP status, or a transport error) via ServerProbeResult and the axios fallback; no secrets or header values are included. - ServersTab keeps the Add form open until the connect test succeeds and shows the reason in a toast on failure (edit flow surfaces it too). - AddServerForm validates the server address and username before probing, with clear per-field messages instead of a silent no-op. - New i18n keys (serverUrlRequired, serverUsernameRequired, serverConnectFailedReason) added across all shipped locales. * fix(servers): route all WebView Subsonic REST through native proxy for gated servers Once a header-gated server (Cloudflare Access, Pangolin, …) connected, every view that still fetched over axios in the WebView came up empty — Main stage, New Releases, Random Albums, Statistics, search, genres — because a non-safelisted gate header makes the WebView send a CORS preflight the gate rejects. Only the Rust-routed paths (streaming, covers, ping) worked. - Add a generic `subsonic_proxy_request` Tauri command backed by `SubsonicClient::send_raw`: it runs the request natively (no preflight), applies the per-server gate header via ServerHttpContext, and returns the untouched JSON body for the WebView to parse as it does an axios response. Supports GET and form-POST (OpenSubsonic formPost) with a clamped timeout. - `api`, `apiWithCredentials`, `apiPostFormWithCredentials` (and thus `apiForServer`/`apiPostFormForServer`) now switch to the native proxy exactly when a request would carry gate headers; header-less servers keep the lightweight WebView axios path unchanged. - Tests: wiremock coverage for `send_raw` (GET gate header + form-POST body); frontend routing tests that gated `api()` calls hit the proxy and header-less ones stay on axios; updated the OpenSubsonic extensions test for the new path. * fix(servers): apply gate header to media paths via URL fallback Streaming, prefetch and artist-info fetches returned 403 on a gated server: apply_for_http_url resolved custom headers strictly by server_ref and attached nothing when the caller's ref (e.g. the audio engine's playback server id) didn't match the registry key. Add a resolve_context helper that falls back to matching the request URL against the server's registered endpoints — the same fallback covers and Navidrome browse already relied on — and route the audio engine, apply_for_http_url and apply_optional_registry_headers through it. Endpoint matching only hits a configured gated server and still honours the apply-to LAN/public rule, so non-gated servers stay untouched. Also pool the native proxy's reqwest clients by timeout bucket so the extra gated-server browse traffic reuses keep-alive connections instead of opening a fresh pool per request (which surfaced as spurious timeouts / 499s on fast endpoints). * fix(servers): register gate headers before probe/bind so native paths work Root cause of the persistent 403s behind a header gate: the native ServerHttpRegistry was populated only at the end of bindIndexedServer, after ensureConnectUrlResolved + the bind session. When that probe was slow, hung, or reported the server briefly offline, the sync never ran, leaving the registry empty — so every Rust-initiated request (stream, cover, prefetch, fanart getArtistInfo2, Navidrome /auth/login) resolved no header and the gate returned 403. The inline-context proxy path kept working, which is why browse succeeded while media/covers failed. Register the header context up front: before the reachability probe in bindIndexedServer, and authoritatively for all servers at the start of bootstrapAllIndexedServers (once React has mounted and IPC is ready). Add concise sync/sync_all diagnostics (endpoint URLs + header count, never values) so gated-server registration is observable in logs. * fix(servers): retry gated-server covers that 403'd during header-registry gap Covers fetched while the native gate-header registry was momentarily empty (e.g. a dev restart before the header sync landed) got a 403, which the cover fetcher classified as a permanent "cover missing" and wrote a 30-minute `.fetch-failed` marker for — so on a gated server most artwork stayed blank long after the gate started answering 200. - Treat a gate-style 401/403 (plus 408/425/429) on cover art as a transient, retryable hiccup rather than a permanent miss, so the short retry loop can ride out a brief registry gap. Genuine 404/410/400 stay permanent. - On (re)bind of a gated server, once its header is registered, clear that server's stale `.fetch-failed` markers and kick a backfill pass so the covers re-download — same rationale as the URL-change retry. * refactor(servers): funnel gate-header application through one helper Collapse the remaining bespoke header-application variants onto the single `apply_optional_registry_headers` / `resolve_context` entry point so gated-server header logic lives in exactly one place: - cover_cache fetch and Navidrome `/auth/login` used older `apply_for_http_url` / `get_for_server_url` + `apply_server_headers_for_http_url` combos; both now call the shared helper. - The audio engine's `apply_playback_request_headers` delegates to it instead of re-implementing the resolve+apply. - Drop the now-unused `ServerHttpRegistry::apply_for_http_url` / `apply_for_base_url` methods. Behaviour is unchanged: a non-gated server (no registry match) leaves every request untouched, exactly as before. This is purely removing duplicate ways to do the same thing so new native paths have one obvious hook. * chore(servers): drop server-http registry debug logging Remove the `[server-http] sync/sync_all` eprintln diagnostics added while tracing the header-registry timing bug. They printed to stderr on every sync (startup + each persist-rehydrate) and echoed endpoint URLs; the behaviour is verified and covered by tests now, so the noise is no longer warranted. The dev-gated `[connect-probe]`/`[subsonic-proxy]` failure logs stay. * docs(changelog): point gated-server entry at PR #1273 * fix(servers): await gate-header sync before probe/bind bindIndexedServer registered the per-server gate headers with a fire-and-forget `void syncServerHttpContextForProfile(...)`, so a direct bind (add / enable a single server) could run the native reachability probe and bind session while the header IPC was still in flight — the registry was empty and native paths 403'd behind the gate, the exact race this branch fixes. Await the sync before probing/binding; the stale-cover retry stays off the critical path. * fix(servers): reclaim LAN from a sticky public endpoint For a dual-address profile, the first endpoint that answered after launch stuck for the whole session: a public sticky endpoint was always tried first and kept answering, so the LAN-first order was never re-run and the connection never upgraded back to LAN. pickReachableBaseUrl now makes a short, no-retry, bounded quick-probe of the higher-priority LAN endpoint before honouring a public sticky entry, so a laptop returning to the LAN upgrades on the next reachability tick while staying remote costs only one bounded probe (never the full retry cushion). * fix(servers): refresh LAN/public badge on active-server switch; credit #1273 The connection badge read the endpoint kind from the last probe and never re-probed when the active server changed, so switching between a LAN-only and a public server left the badge stuck on the old server's classification until the 120-s tick. useConnectionStatus now resets the kind and re-probes when activeServerId changes (mount still deferred to the polling effect). Also adds the settings credits line for the gated-server work (PR #1273). * fix(servers): LAN reclaim uses the probe's own timeout, not a 3s race The reclaim quick-probe raced pingWithCredentialsForProfile against a fixed 3s timer, but the underlying probe (native reqwest for gated servers, axios otherwise) runs on a 15s timeout — so a LAN endpoint answering between 3s and 15s was wrongly treated as unreachable and the session stayed pinned to the public sticky endpoint. Reclaim now does a single, no-retry probe on the LAN endpoint using the normal ping timeout: a slow-but-reachable LAN still upgrades, while a dead one still costs only one attempt (not the full retry cushion) before falling through to the sticky sequence.