fix(plex): match server by /identity machineIdentifier for hostname URLs - #74
Merged
Conversation
Hostname-based PLEX_SERVER_URL values (e.g. http://plex.local.timo.be:32400) failed membership matching when the server only advertised .plex.direct URIs, leaving users stuck on onboarding step 2. Add Strategy 0 in findConfiguredServer that fetches /identity and compares the returned machineIdentifier against each resource's clientIdentifier. Cache the configured machineId in app_settings and invalidate on URL/token change. Surface granular membership failure reasons (not_reachable / not_in_resources / not_owner) and expose them in onboarding: show a reachability error card with a "Use a different server" fallback, and — only when /identity succeeds but Plex.tv doesn't list the server — an explicit "I confirm I own this server" override that logs an audit warn. Fixes edbfi/obzorarr-docker#5.
🤖 Augment PR SummarySummary: Fixes Plex onboarding failures when Changes:
Technical Notes: Membership checks now always perform a live 🤖 Was this summary useful? React with 👍 or 👎 |
- guard confirmOwnershipOverride with isOnboardingComplete() to prevent post-onboarding admin escalation (onboardingHandle skip-list exposes /onboarding actions after setup) - atomically update sessions.isAdmin, users.isAdmin, and users.accountId in one db.transaction to avoid inconsistent state on partial failure - require locals.user.isAdmin in forceManualSelection, matching continueAfterServerSelection
Collaborator
Author
|
augment review |
- invalidate cached SERVER_MACHINE_ID in clearConflictingDbSettings when PLEX_SERVER_URL or PLEX_TOKEN env is set, so a restart with new env values doesn't return a stale machineId from a previous server
Collaborator
Author
|
augment review |
- drop "confirm ownership to bypass" hint from messageForMembershipFailure, since that override is only reachable during onboarding but this message is surfaced post-onboarding via requireServerMembership - reject forceManualSelection with fail(400) when PLEX_SERVER_URL or PLEX_TOKEN is set as an env var; getPlexConfig always prefers env over DB, so the manual picker would otherwise save values the rest of the app ignores
Collaborator
Author
|
augment review |
…ride write - Add optional identityErrorReason to MembershipResult and thread it through verifyServerMembership when /identity fails without a machineId - Export messageForMembershipFailure and interpolate identityErrorReason into the not_reachable branch so auth/timeout/other causes are surfaced instead of a generic "could not reach" message - Use messageForMembershipFailure(membership) in the onboarding verifyAdmin action to keep a single source of truth for failure copy - Remove setPlexServerUrlOverrideManual(true) from forceManualSelection: the iter 3 env guard makes the write unreachable when the flag would matter, and writing it when env is absent is dead state that would silently bypass the guard if env vars were added in a future boot
Collaborator
Author
|
augment review |
… probe on onboarding load - Move clearTimeout into a finally block so CONNECTION_TIMEOUT_MS caps response.json() + schema parsing, not just the header fetch resolving. A slow or partial body stream now aborts via the AbortController signal and surfaces through classifyConnectionError like any other timeout. - Swap the onboarding load's getConfiguredServerMachineId() for refreshConfiguredServerMachineId() so the reachability banner reflects the live server state. A cached machineId could previously mask a now-offline server or revoked token and hide the remediation path. confirmOwnershipOverride keeps the cached call — it fires seconds after the load, so cache reuse is intentional.
Collaborator
Author
|
augment review |
Collaborator
Author
|
augment review |
Collaborator
Author
|
augment review |
Collaborator
Author
|
augment review |
…hen server absent from plex.tv resources
Collaborator
Author
|
augment review |
verifyServerMembership() previously admitted URL-based matches even when the /identity probe against the configured server failed, letting users proceed into onboarding states where subsequent Plex sync would fail. Short-circuit to reason='not_reachable' when getConfiguredServerMachineId() returns null so the onboarding UI surfaces the unreachable state instead of advancing to sync. Simplify the not-in-resources branch since configuredMachineIdFromIdentity is now guaranteed past the gate. Also add a regression test asserting refreshConfiguredServerMachineId() evicts stale cache entries when /identity fails, so a failed refresh cannot leave a pre-existing machineId in place.
Collaborator
Author
|
augment review |
The "no matching server" test previously only mocked global.fetch, so the same mock response was also consumed by the /identity probe path (as an accidentally-invalid shape). The result then depended on that parse-failure fallback rather than the intended no-matching-server scenario. Mock getConfiguredServerMachineId() explicitly to return a known machineId and assert reason='not_in_resources' plus the returned configuredMachineId so the test deterministically exercises the intended branch.
Collaborator
Author
|
augment review |
The gate read the configured machineId via the cache-first helper, so a stale SQLite-backed entry could satisfy it after the configured PLEX_TOKEN was revoked or the server went offline. Only the onboarding page's load() pre-refreshed, leaving the 15-minute session revalidation and other callers exposed. Switch the call in verifyServerMembership to refreshConfiguredServerMachineId so the gate is robust-by-default.
Collaborator
Author
|
augment review |
engels74
added a commit
that referenced
this pull request
Apr 19, 2026
Align the two onboarding connection-test endpoints with the reference pattern already used in fetchServerIdentity (PR #74). Previously the timer was cleared immediately after fetch resolved its headers, leaving response.json() and PlexServerIdentitySchema.safeParse() unprotected by CONNECTION_TIMEOUT_MS. A Plex server that sent headers and then stalled mid-body could hang the request indefinitely. Consolidate clearTimeout(timeoutId) into a single finally block so the timeout budget covers the full /identity call: fetch, body parsing, and schema validation. Pure control-flow refactor - no behaviour change on the happy path or already-timed-out path. - src/routes/api/onboarding/test-connection/+server.ts - src/routes/api/onboarding/select-server/+server.ts (testConnection helper) select-server keeps its 15000ms budget; test-connection keeps 10000ms.
4 tasks
engels74
added a commit
that referenced
this pull request
Apr 19, 2026
#75) Align the two onboarding connection-test endpoints with the reference pattern already used in fetchServerIdentity (PR #74). Previously the timer was cleared immediately after fetch resolved its headers, leaving response.json() and PlexServerIdentitySchema.safeParse() unprotected by CONNECTION_TIMEOUT_MS. A Plex server that sent headers and then stalled mid-body could hang the request indefinitely. Consolidate clearTimeout(timeoutId) into a single finally block so the timeout budget covers the full /identity call: fetch, body parsing, and schema validation. Pure control-flow refactor - no behaviour change on the happy path or already-timed-out path. - src/routes/api/onboarding/test-connection/+server.ts - src/routes/api/onboarding/select-server/+server.ts (testConnection helper) select-server keeps its 15000ms budget; test-connection keeps 10000ms.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Fixes engels74/obzorarr-docker#5.
When
PLEX_SERVER_URLis a hostname (e.g.http://plex.local.timo.be:32400, a LAN DNS name, or a reverse-proxied URL) and the Plex server advertises only.plex.directconnection URIs under the signed-in user's Plex.tv resources list, Obzorarr's four URL-string matching strategies all fail andverifyServerMembershipreturns{ isMember: false }. The user sees a greyed-out Continue button with no actionable error message and cannot complete onboarding.The authoritative test is not URL-string matching — Obzorarr already reaches the configured server successfully, so it can ask the server for its own identity via
GET /identityand match the returnedmachineIdentifieragainst each Plex.tv resource'sclientIdentifier. This is the same value Plex itself uses for server identity and matches regardless of how the URL is expressed.Changes
New
Strategy 0: match by machineIdentifier from/identitysrc/lib/server/plex/server-identity.service.tsexposesfetchServerIdentity(),getConfiguredServerMachineId()(cache-first), andrefreshConfiguredServerMachineId()(force refresh). Uses a 10 sAbortControllertimeout and the existingclassifyConnectionErrorhelper.src/lib/server/auth/membership.tsprepends a new matching strategy that compares the fetchedmachineIdentifieragainst each server'sclientIdentifier. The existing four URL-based strategies remain as fallbacks for when/identityis unreachable.MembershipResultgains a discriminatedreason?: 'not_reachable' | 'not_in_resources' | 'not_owner'andconfiguredMachineId?: stringso the UI can render specific messages and offer an explicit ownership override.Cache + invalidation
app_settingsgains aSERVER_MACHINE_IDrow, mirroring the existingSERVER_NAMEcache (getCachedServerMachineId/setCachedServerMachineId/clearCachedServerMachineId).PLEX_SERVER_URLorPLEX_TOKENchanges so the next membership check re-fetches/identity.Onboarding UX
src/routes/onboarding/plex/+page.server.tsproactively callsgetConfiguredServerMachineId()inloadand returnsconfiguredUrlReachable,configuredUrlErrorReason, andconfiguredMachineIdso the page can warn before the user signs in.forceManualSelectionaction sets an app-settings flag that forces the manual server picker on the next load;src/routes/onboarding/+layout.server.tsrespects the flag.confirmOwnershipOverrideaction: when/identitysucceeded but Plex.tv does not list the server for the signed-in user, the user can explicitly attest ownership to bypass the resources check. Logged atlogger.warnwith URL, machineId, and username for audit trail.src/routes/onboarding/plex/+page.svelterenders specific error cards with remediation buttons instead of the previous silent disabled state.Tests
tests/unit/auth/membership.test.ts— four new cases covering Strategy 0 match, fallback when/identityunreachable,not_in_resourceswith machineId, and the cache interaction.tests/unit/plex/server-identity.service.test.ts— new file covering the valid response, missingfriendlyName, 401 / non-401 HTTP errors,AbortErrorclassification, URL trailing-slash normalization, cache hit/miss, and the force-refresh path.Reproduction and verification
Bug reproduced locally against a Bun proxy (
/tmp/plex-proxy.ts) that forwards a plainhttp://localhost:7777origin to the live Plex server. Pre-fix, the logs show the exact failure from the upstream issue:Sign-in throws
NotServerMemberErrorand the onboarding page renders the authentication-failure card:Pre-fix, onboarding step 2 leaves the user stuck at CONNECT (URL blurred):
Post-fix, with the same hostname-only
PLEX_SERVER_URL, Strategy 0 fetches/identity, matches the returnedmachineIdentifieragainst Plex.tv resources, and the user advances to SYNC:Test plan
bun run check— 0 errorsbun run test— full suite passes, new tests included, coverage ≥ 80 %bun run check:biome— lint + format cleanNo matching server found; post-fix matches viamachineIdentifierand advances to SYNCforceManualSelectionbutton falls back to manual pickerPLEX_SERVER_URL/PLEX_TOKENchange in admin settings