Files
mule-image/.hermes/plans/2026-06-06_210000-remaining-fixes.md
dtoro 243e5d3831 fix: sidecar response interceptor + folder proxy
- 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
2026-06-06 23:01:38 +02:00

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:

  1. 404 on photo grid — "Request failed with status code 404" in private window
  2. 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

  1. 404 on photo grid — even in private window (no cache interference)
  2. 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:

  1. The session data is loaded asynchronously
  2. session.user.BasePath might not yet be populated when listFolders fires
  3. 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

  1. Edit web/src/lib/services/photoprism.ts
  2. Add the 401-handling response interceptor to the sidecar instance
  3. The interceptor mirrors the existing http response interceptor exactly

Step 2: Rebuild frontend

cd /opt/mule-image/web && npm run build

Step 3: (Optional) Add sidecar folder proxy

  1. New file sidecar/handlers_folders_proxy.go
  2. Handler similar to handlePhotos — proxies to /api/v1/folders/originals, post-filters by Path prefix
  3. Wire route in main.go: auth.GET("/folders", handleFoldersProxy(pp))
  4. Build Docker image, restart sidecar

Step 4: Update listFolders to use sidecar

  1. Change listFolders() to use sidecar instance
  2. Call /api/sidecar/folders instead 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:

  1. Open private window → navigate to photos.hubris.network
  2. Log in as muli via Authentik OIDC
  3. Verify photo grid loads without 404
  4. Verify folder tree shows only muli's folders
  5. 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 sidecar instance 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 listFolders until 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.