Custom feeds, search, and threads route to wrong host for non-bsky.social PDS OAuth users #35

Closed
opened 2026-06-21 20:14:50 +00:00 by starsetbyte · 0 comments
Owner

Summary

For OAuth users whose PDS is not bsky.social, custom feeds, search, and thread views fail because PdsRoutingInterceptor rewrites all XRPC reads to the user's own PDS — but most read endpoints are served by the app view (public.api.bsky.app), not the PDS. Only app.bsky.labeler.getServices is currently exempted via APP_VIEW_ONLY_PATHS.

Root cause

PdsRoutingInterceptor (auth/oauth/PdsRoutingInterceptor.kt) rewrites every /xrpc/* path to session.pdsUrl for OAuth sessions. The single allowlist entry (app.bsky.labeler.getServices) was added to fix labeler loading (see docs/OAUTH_IMPLEMENTATION_STATUS.md), but the same class of bug affects every app-view endpoint:

Endpoint Served by Currently routed to Bug
app.bsky.feed.getTimeline PDS (user session) PDS works
app.bsky.actor.getPreferences PDS PDS works
app.bsky.notification.listNotifications PDS PDS works
com.atproto.repo.* (writes) PDS PDS works
app.bsky.feed.getFeed app view PDS custom feeds empty/error
app.bsky.feed.getFeedGenerators app view PDS feed names/avatars fail to hydrate
app.bsky.feed.getPostThread app view PDS thread view broken
app.bsky.feed.searchPosts app view PDS search broken
app.bsky.actor.searchActors app view PDS people search broken
app.bsky.actor.getAuthorFeed app view PDS other profiles' feeds broken
app.bsky.actor.getProfile app view PDS profile hydration unreliable

Proposed fix

Expand APP_VIEW_ONLY_PATHS (or rename to APP_VIEW_PATHS) to include the app-view-served read endpoints listed above.

Alternatively (more robust), invert the routing model: route writes and a small set of session-scoped reads (getTimeline, getPreferences, putPreferences, listNotifications, updateSeen) to the PDS, and everything else to the app view. This matches what the official client does and won't need updating each time a new read endpoint is added.

Considerations

  • The app-view host public.api.bsky.app already works for the labeler allowlist entry, so no new infrastructure is needed.
  • getAuthorFeed for the user's own DID could theoretically go to the PDS, but the app-view also serves it correctly — simpler to always use app-view for reads.
  • Needs device verification against a non-bsky.social PDS account.

Reproduction

  1. Log in via OAuth with an account whose PDS is not bsky.social (e.g. a self-hosted PDS or a third-party-hosted account).
  2. Open a custom feed tab - empty or error.
  3. Tap a post - thread fails to load.
  4. Search - no results.
  5. All of the above work fine for bsky.social-hosted accounts because the PDS and the app-view's parent domain coincide.
## Summary For OAuth users whose PDS is not `bsky.social`, custom feeds, search, and thread views fail because `PdsRoutingInterceptor` rewrites **all** XRPC reads to the user's own PDS — but most read endpoints are served by the **app view** (`public.api.bsky.app`), not the PDS. Only `app.bsky.labeler.getServices` is currently exempted via `APP_VIEW_ONLY_PATHS`. ## Root cause `PdsRoutingInterceptor` (`auth/oauth/PdsRoutingInterceptor.kt`) rewrites every `/xrpc/*` path to `session.pdsUrl` for OAuth sessions. The single allowlist entry (`app.bsky.labeler.getServices`) was added to fix labeler loading (see `docs/OAUTH_IMPLEMENTATION_STATUS.md`), but the same class of bug affects every app-view endpoint: | Endpoint | Served by | Currently routed to | Bug | |---|---|---|---| | `app.bsky.feed.getTimeline` | PDS (user session) | PDS | works | | `app.bsky.actor.getPreferences` | PDS | PDS | works | | `app.bsky.notification.listNotifications` | PDS | PDS | works | | `com.atproto.repo.*` (writes) | PDS | PDS | works | | `app.bsky.feed.getFeed` | **app view** | PDS | **custom feeds empty/error** | | `app.bsky.feed.getFeedGenerators` | **app view** | PDS | **feed names/avatars fail to hydrate** | | `app.bsky.feed.getPostThread` | **app view** | PDS | **thread view broken** | | `app.bsky.feed.searchPosts` | **app view** | PDS | **search broken** | | `app.bsky.actor.searchActors` | **app view** | PDS | **people search broken** | | `app.bsky.actor.getAuthorFeed` | **app view** | PDS | **other profiles' feeds broken** | | `app.bsky.actor.getProfile` | **app view** | PDS | **profile hydration unreliable** | ## Proposed fix Expand `APP_VIEW_ONLY_PATHS` (or rename to `APP_VIEW_PATHS`) to include the app-view-served read endpoints listed above. Alternatively (more robust), invert the routing model: route **writes** and a small set of session-scoped reads (`getTimeline`, `getPreferences`, `putPreferences`, `listNotifications`, `updateSeen`) to the PDS, and everything else to the app view. This matches what the official client does and won't need updating each time a new read endpoint is added. ### Considerations - The app-view host `public.api.bsky.app` already works for the labeler allowlist entry, so no new infrastructure is needed. - `getAuthorFeed` for the user's own DID could theoretically go to the PDS, but the app-view also serves it correctly — simpler to always use app-view for reads. - Needs device verification against a non-bsky.social PDS account. ## Reproduction 1. Log in via OAuth with an account whose PDS is not `bsky.social` (e.g. a self-hosted PDS or a third-party-hosted account). 2. Open a custom feed tab - empty or error. 3. Tap a post - thread fails to load. 4. Search - no results. 5. All of the above work fine for bsky.social-hosted accounts because the PDS and the app-view's parent domain coincide.
Sign in to join this conversation.
No description provided.