- Added 401-handling response interceptor to sidecar axios instance (matches existing http instance) so expired/invalid tokens redirect to login instead of showing raw 404/401 errors - Added GET /api/sidecar/folders — proxies PhotoPrism's /api/v1/folders/originals with BasePath post-filter - Updated listFolders() frontend to call sidecar proxy - Updated plan with remaining fixes
8.5 KiB
Plan: Fix remaining user isolation issues — 404 errors and folder tree
Date: 2026-06-06
Author: Hermes Agent
Status: Draft
1. Goal
Fix the remaining issues after deploying the sidecar scoping proxy:
- 404 on photo grid — "Request failed with status code 404" in private window
- Folder tree shows other users — on first load, the library tree lists other users' folders; a refresh fixes it
2. Current Context
What's deployed
| Component | Status |
|---|---|
Sidecar labels proxy (/api/sidecar/labels) |
✅ Working |
Sidecar counts proxy (/api/sidecar/counts) |
✅ Working |
Sidecar timeline proxy (/api/sidecar/timeline) |
✅ Working through Caddy |
Caddy fallback for /api/v1/api/sidecar/* |
✅ Working |
Frontend rebuild with sidecar axios instance |
✅ Built and deployed |
Verified working via Caddy
# Through public URL with valid admin token
curl https://photos.hubris.network/api/sidecar/timeline?count=1 → HTTP 200
curl https://photos.hubris.network/api/v1/photos?count=1 → HTTP 200
Both endpoints return 200 when tested directly through Caddy with a valid token.
Reported issues
- 404 on photo grid — even in private window (no cache interference)
- Folder tree shows other users' folders on first load, fixed by refresh
3. Root Cause Analysis
Issue 1: 404 on photo grid
The sidecar axios instance (baseURL: '') is missing the response interceptor that:
- Handles 401 → clears session → redirects to login
- Re-throws with meaningful error message
The http instance (for /api/v1 endpoints) has this interceptor. Without it on sidecar:
- If the sidecar returns a non-2xx (401, 502 from upstream PP failure, etc.), axios throws a raw error
- The TanStack Query error boundary catches it and shows "Request failed with status code "
- Very likely the sidecar is returning 401 on some calls (token expired / session not yet established) and the error message might show 404 because Caddy's catch-all returns 404 when a matcher doesn't find a route
Hypothesis: During OIDC login flow, the frontend may make some sidecar calls BEFORE the session is fully established (token loaded into session.accessToken). The sidecar interceptor checks session.accessToken but it might be null. Then the request to /api/sidecar/timeline has no auth header → sidecar returns 401 → no response interceptor → raw error.
Fix: Add the same 401 → login redirect interceptor to the sidecar instance.
Issue 2: Folder tree shows other users
listFolders() calls http.get('/folders/originals') which hits PhotoPrism directly. PhotoPrism returns all folders across the library regardless of user. The frontend then filters by userBasePath() on the result:
const bp = userBasePath();
if (bp === '') return folders; // On first load, bp might be empty!
return folders.filter((f) => f.Path === bp || f.Path.startsWith(bp + '/'))
On first load, userBasePath() returns "" because:
- The session data is loaded asynchronously
session.user.BasePathmight not yet be populated whenlistFoldersfires- The TanStack Query cache from a previous session might still have old data
After a refresh, the session is fully loaded, and userBasePath() returns the correct value.
A secondary issue: the http interceptor's 401 handler clears the session on 401. If the session expires during the app's lifetime, all subsequent requests fail with 401.
4. Proposed Approach
Phase 1: Fix 404 — add response interceptor to sidecar
File: web/src/lib/services/photoprism.ts
Add the same 401 → login redirect interceptor to sidecar as already exists on http:
sidecar.interceptors.response.use(
(r) => r,
(err: AxiosError) => {
if (err.response?.status === 401 && browser) {
clearSession();
const url = err.config?.url ?? '';
if (!url.endsWith('/session')) {
void goto('/login', { replaceState: true });
}
}
return Promise.reject(err);
}
);
Phase 2: Fix folder tree — sidecar folder proxy
File: sidecar/handlers_folders.go (new)
Add a sidecar endpoint that proxies /folders/originals and post-filters by BasePath:
GET /api/sidecar/folders → proxies to GET /api/v1/folders/originals
→ removes folders not under user's base_path
→ returns filtered list
This avoids the timing issue entirely by filtering on the server side.
Alternative (simpler): Fix the frontend timing issue by ensuring listFolders doesn't fire until the session is ready.
Phase 3: Change folder tree in frontend
File: web/src/lib/services/photoprism.ts
Change listFolders() to use sidecar instance and call /api/sidecar/folders:
export async function listFolders(): Promise<PpFolder[]> {
const { data } = await sidecar.get<{ folders?: PpFolder[] }>(
'/api/sidecar/folders',
{ params: { recursive: true, uncached: true, files: false } }
);
const bp = userBasePath();
const folders = data.folders ?? [];
if (bp === '') return folders;
return folders
.filter((f) => f.Path === bp || f.Path.startsWith(bp + '/'))
.map((f) => ({ ...f, Path: toUserPath(f.Path) }));
}
5. Step-by-step Plan
Step 1: Add sidecar response interceptor
- Edit
web/src/lib/services/photoprism.ts - Add the 401-handling response interceptor to the
sidecarinstance - The interceptor mirrors the existing
httpresponse interceptor exactly
Step 2: Rebuild frontend
cd /opt/mule-image/web && npm run build
Step 3: (Optional) Add sidecar folder proxy
- New file
sidecar/handlers_folders_proxy.go - Handler similar to
handlePhotos— proxies to/api/v1/folders/originals, post-filters byPathprefix - Wire route in
main.go:auth.GET("/folders", handleFoldersProxy(pp)) - Build Docker image, restart sidecar
Step 4: Update listFolders to use sidecar
- Change
listFolders()to usesidecarinstance - Call
/api/sidecar/foldersinstead of/folders/originals
Step 5: Rebuild + validate
# Rebuild frontend
cd /opt/mule-image/web && npm run build
# Test through Caddy
curl -s "https://photos.hubris.network/api/sidecar/timeline?count=1" \
-H "X-Auth-Token: <token>" | head -c 200
# Verify folders
curl -s "https://photos.hubris.network/api/sidecar/folders" \
-H "X-Auth-Token: <token>" | python3 -c "import sys,json;d=json.load(sys.stdin);print(json.dumps(d[:3],indent=2))"
Step 6: Commit
git add -A && git commit -m "fix: add sidecar response interceptor + folder proxy" && git push
6. Files Likely to Change
| File | Change |
|---|---|
web/src/lib/services/photoprism.ts |
Add response interceptor to sidecar instance; change listFolders URL |
sidecar/handlers_folders_proxy.go |
New — folder proxy handler |
sidecar/main.go |
Wire folder proxy route |
7. Tests & Validation
Manual:
- Open private window → navigate to photos.hubris.network
- Log in as muli via Authentik OIDC
- Verify photo grid loads without 404
- Verify folder tree shows only muli's folders
- Switch to dtoro account → verify folders/timeline scoped to dtoro
API tests:
# Sidecar timeline (no token → 401 redirect)
curl -s "https://photos.hubris.network/api/sidecar/timeline?count=1"
# Sidecar folders
curl -s "https://photos.hubris.network/api/sidecar/folders"
8. Risks & Open Questions
Risks
| Risk | Impact | Mitigation |
|---|---|---|
| Sidecar returns 401 during OIDC login flow before session is ready | 404 showing instead of graceful redirect | Add response interceptor in Phase 1 |
| Folder proxy adds latency | Slower folder tree loading | Minimal — single proxy call, same as PP direct |
userBasePath() timing issue in listFolders persists even with sidecar |
Folder tree still shows wrong folders on first load | Sidecar filter is server-side → no timing dependency |
Open Questions
- Q1: Are there other API calls that bypass the
sidecarinstance and might also be unscoped? (e.g.,listSubjects,listGeo, etc.) - Q2: Does the sidecar need a folder proxy, or is the timing fix sufficient? The timing fix (delaying
listFoldersuntil session is ready) is simpler but fragile. - Q3: Could the 404 be from Caddy's catch-all returning 404 when the sidecar isn't reachable? The Caddy fallback timeout for the sidecar might need tuning.