HTTP Interceptors
Audience: End users, system administrators, technical stakeholders Last Updated: 2026-04-06 Version: 2.0.0
Table of Contents
- Overview
- How HTTP Interceptors Work
- The Six Interceptors
- Authentication Flow
- Error Handling & User Notifications
- Retry & Resilience
- Request Timing & Logging
- Session Expiry Detection
- Endpoint Exclusions
- Technical Specifications
- Security Considerations
- Troubleshooting
- Related Documentation
Overview
HTTP interceptors are middleware that process every network request the app makes before it leaves the device and every response that comes back. Llamafin uses a chain of six interceptors to automatically handle server URL management, authentication, error handling, retry logic, request timing, and session expiry detection.
Why It Matters
Without interceptors, every API call in the app would need to manually:
- Prepend the server URL to the request path
- Attach authentication tokens to headers
- Handle errors and show user notifications
- Retry failed requests on server errors
- Track request timing for debugging
- Detect expired sessions and log the user out
Interceptors centralize this logic so that every API call automatically benefits from these behaviors without any additional code.
Key Benefits
| Benefit | Description |
|---|---|
| Automatic Authentication | Tokens are attached to every request without manual handling |
| Consistent Error Handling | All errors are caught, logged, and shown to the user as toast messages |
| Self-Healing Retries | Server errors are automatically retried with exponential backoff |
| Session Protection | Expired sessions are detected and the user is safely logged out |
| Performance Monitoring | Every request is timed and logged for debugging |
| URL Management | Server URL changes are handled centrally without updating every API call |
How HTTP Interceptors Work
The Chain Concept
Think of interceptors as a series of checkpoints that every request passes through on its way to the server, and every response passes through on its way back. Each checkpoint can inspect, modify, or block the request/response.
App Makes Request
│
▼
┌─────────────────────────────────────────┐
│ Checkpoint 1: Server URL │
│ → Prepends server URL │
│ → Adds base authentication header │
└─────────────────┬───────────────────────┘
│
▼
┌─────────────────────────────────────────┐
│ Checkpoint 2: Error Handler │
│ → Passes through (handles errors │
│ on the way back) │
└─────────────────┬───────────────────────┘
│
▼
┌─────────────────────────────────────────┐
│ Checkpoint 3: Authentication │
│ → Waits for app to load your session │
│ → Attaches your auth token │
└─────────────────┬───────────────────────┘
│
▼
┌─────────────────────────────────────────┐
│ Checkpoint 4: Retry Logic │
│ → Sets up automatic retry if the │
│ server returns an error │
└─────────────────┬───────────────────────┘
│
▼
┌─────────────────────────────────────────┐
│ Checkpoint 5: Request Timer │
│ → Records when the request starts │
│ → Logs how long it took │
└─────────────────┬───────────────────────┘
│
▼
┌─────────────────────────────────────────┐
│ Checkpoint 6: Session Monitor │
│ → Checks for "unauthorized" errors │
│ → Logs you out if your session expired│
└─────────────────┬───────────────────────┘
│
▼
Sent to Server
Response Flow (Reverse)
When the server responds, the response travels back through the same checkpoints in reverse order (from Checkpoint 6 back to Checkpoint 1). This allows interceptors to:
- Detect 401/403 errors before the app sees them
- Retry failed requests before giving up
- Log timing information
- Show user-friendly error messages
The Six Interceptors
1. Server URL Interceptor
What It Does: Automatically prepends your Jellyfin server URL to every API request and attaches a base authentication header.
How It Works:
- Reads your configured server URL from app settings
- Prepends it to the request path (e.g.,
/Items/123becomeshttp://your-server:8096/Items/123) - Attaches an
X-Emby-Authorizationheader with device and app information
Bypasses: Skips requests for:
- Static assets (
/assets/*) - Absolute URLs (already complete URLs starting with
http) - Requests explicitly marked to bypass (e.g., server discovery pings)
Header Format:
X-Emby-Authorization: MediaBrowser Client="llamafin", Device="iPhone", DeviceId="abc-123", Version="2.0.0"
This header identifies your app, device, and version to the Jellyfin server. The authentication token is added by the next interceptor.
2. HTTP Error Interceptor
What It Does: Catches HTTP errors and displays user-friendly toast messages while logging details for debugging.
Error Types Handled:
| Error Type | What Triggers It | What You See |
|---|---|---|
| Network Error | No internet, DNS failure, server unreachable | Toast: "Unknown network error occurred" |
| Client Error (4xx) | Bad request, not found, unauthorized (excluding 401/403) | Toast: "Client error (status code)" |
| Server Error (5xx) | Server crash, internal error, service unavailable | Toast: "Server error (status code)" |
| Lyrics Not Found (404) | No lyrics available for a song | Nothing (silently handled, no error shown) |
Special Case -- Lyrics 404: When the app requests lyrics for a song and none exist, the server returns a 404. This is expected behaviour (not every song has lyrics), so the interceptor converts this into a successful empty response rather than showing an error.
3. Authentication Interceptor
What It Does: Waits for your session to be loaded, then attaches your authentication token to every request.
How It Works:
-
Public Access: Requests to public endpoints (server info, login page, public user list) pass through immediately -- no authentication needed.
-
Session Wait: For authenticated requests, the interceptor pauses until your session has been fully rehydrated from storage. This prevents race conditions when the app starts up.
-
Token Attachment: Once your session is loaded, it appends your access token to the authentication header:
X-Emby-Authorization: ..., Token="your-access-token" -
Expired Session Detection: If your session is marked as expired before the request even fires, the interceptor stops the request and logs you out immediately.
Why Session Wait Matters: When you open the app, it needs time to read your saved session from storage. Without this wait, API calls could fire before your token is loaded, resulting in unauthorized errors. The interceptor ensures every request has your token attached.
4. Retry Interceptor
What It Does: Automatically retries failed requests when the server returns a 5xx error or a network failure occurs.
Retry Logic:
| Condition | Retry? | Delay Before Retry |
|---|---|---|
| Server error (5xx) | Yes, up to 3 attempts | 100ms, then 200ms, then 400ms |
| Network failure (no response) | Yes, up to 3 attempts | 100ms, then 200ms, then 400ms |
| Client error (4xx) | No | N/A |
| Not found (404) | No | N/A |
Exponential Backoff: Each retry waits twice as long as the previous one:
- 1st retry: 100ms delay
- 2nd retry: 200ms delay
- 3rd retry: 400ms delay
Total maximum retry delay: 700ms (less than one second).
Why Not Retry Client Errors: A 4xx error (like "not found" or "bad request") means the request itself is invalid. Retrying won't fix it -- the same error will occur again. Only transient server issues (5xx) and network drops are worth retrying.
5. Request Timestamp Interceptor
What It Does: Measures how long each HTTP request takes and logs the timing for debugging.
What It Logs:
| Event | Log Level | Information Recorded |
|---|---|---|
| Success | Debug | HTTP method, URL, status code, start time, end time, duration (ms) |
| Failure | Warning | Same as success, plus error status text |
Example Log Entry:
Request Succeeded: GET /Items/123?fields=Path
Status: 200
Start: 2026-04-06T10:30:15.123Z
End: 2026-04-06T10:30:15.345Z
Duration: 222ms
Note on Retry Timing: If a request is retried, the logged duration includes all retry delays. A request that retries twice will report the total time including the 100ms + 200ms backoff delays, not just the final successful request time.
6. Unauthorised Interceptor
What It Does: Detects when the server rejects your authentication (401 or 403 errors) and safely logs you out.
How It Works:
-
401/403 Detection: Catches any response with status 401 (Unauthorized) or 403 (Forbidden).
-
Session Verification: Before taking action, it checks if you actually have an active session. This prevents accidental logouts for unauthenticated requests made before you've logged in.
-
Session Cleanup: If you have a valid token and the server rejects it:
- Logs a warning with the failing URL
- Dispatches a "session expired" action
- Shows a toast: "Session expired"
- Redirects you to the login page
-
Re-throws the Error: The error is passed back to the original requester so it can handle the failure appropriately.
Why Session Verification Matters: Without this check, the interceptor would log you out for any 401 error, including requests made by the app before you've ever logged in. The verification ensures logout is only triggered for requests made by an authenticated user whose token has been rejected.
Authentication Flow
How Authentication Travels with Requests
┌─────────────────────────────────────────────────────────────┐
│ X-Emby-Authorization Header │
├─────────────────────────────────────────────────────────────┤
│ │
│ Stage 1 (Server URL Interceptor): │
│ MediaBrowser Client="llamafin", Device="iPhone", │
│ DeviceId="abc-123", Version="2.0.0" │
│ │
│ Stage 2 (Auth Interceptor): │
│ MediaBrowser Client="llamafin", Device="iPhone", │
│ DeviceId="abc-123", Version="2.0.0", │
│ Token="your-access-token-here" │
│ │
└─────────────────────────────────────────────────────────────┘
This two-stage construction ensures:
- The server always receives device identification (even for unauthenticated requests)
- The token is only added when a valid session exists
- The format is compatible with Jellyfin/Emby server authentication
Error Handling & User Notifications
What You See When Things Go Wrong
| Scenario | User Notification | What Happens Behind the Scenes |
|---|---|---|
| Server crashes mid-request | Toast: "Server error (500)" after 3 retries | Retries 3 times with backoff, then shows error |
| You lose internet | Toast: "Unknown network error" after 3 retries | Retries 3 times, then shows generic network error |
| Your session expires | Toast: "Session expired" → redirected to login | 401 detected, session cleared, redirect to login |
| Song not found (404) | Toast: "Client error (404)" | Logged as warning, error re-thrown to caller |
| No lyrics available | Nothing (transparently handled) | 404 converted to successful empty response |
| Bad request (400) | Toast: "Client error (400)" | Logged as warning, error re-thrown to caller |
Error Toast Messages
All error toasts use translation keys so they appear in your selected language:
| Translation Key | When Displayed |
|---|---|
errors.HTTP_ERROR_UNKNOWN | Network error, DNS failure, server unreachable |
errors.HTTP_ERROR_CLIENT | 4xx error (400, 404, etc.) with status code parameter |
errors.HTTP_ERROR_SERVER | 5xx error (500, 502, etc.) with status code parameter |
app.TOAST.SESSION_EXPIRED | 401/403 detected with active session |
Retry & Resilience
Automatic Retry Behaviour
When the server experiences a transient error (5xx) or your network drops momentarily, the retry interceptor automatically attempts to recover:
Retry Timeline Example:
Time 0ms: Request sent → Server returns 500 error
Time 100ms: 1st retry → Server returns 500 error
Time 300ms: 2nd retry (100ms + 200ms backoff) → Server returns 500 error
Time 700ms: 3rd retry (300ms + 400ms backoff) → Server returns 200 OK ✓
If all 3 retries fail, the error is passed to the HTTP Error Interceptor for user notification.
What Is NOT Retried
| Error Type | Why Not Retried |
|---|---|
| 400 Bad Request | The request is malformed; retrying won't fix it |
| 401 Unauthorized | Your token is invalid; needs re-authentication, not retry |
| 403 Forbidden | You lack permissions; retrying won't grant access |
| 404 Not Found | The resource doesn't exist; retrying won't create it |
Request Timing & Logging
How Timing Works
Every request is timed from the moment it leaves the app (after passing through all interceptors) to the moment the response is received.
What's Logged:
- HTTP method (GET, POST, etc.)
- Full URL with query parameters
- Response status code
- Start and end timestamps (ISO format)
- Total duration in milliseconds
Log Levels:
- Debug: Successful requests (for troubleshooting)
- Warning: Failed requests (for error investigation)
Accessing Timing Data
Request timing logs appear in the app's debug logging system, accessible through:
- Settings → Advanced → Debugging → App Logs
- Diagnostic export from the loading shell during app boot
Session Expiry Detection
How Session Expiry Is Detected
The app uses a two-layer approach to detect expired sessions:
| Layer | Interceptor | Detection Method |
|---|---|---|
| Preemptive | Auth Interceptor | Checks is_expired flag before sending request |
| Reactive | Unauthorised Interceptor | Catches 401/403 response from server |
Preemptive Detection (Auth Interceptor)
Before a request fires, the Auth Interceptor checks if your session's is_expired flag is set. If so:
- The request is blocked (never sent to the server)
- A "session expired" toast is shown
- You're redirected to the login page
Reactive Detection (Unauthorised Interceptor)
If the preemptive check passes but the server still rejects your token:
- The 401/403 response is caught
- The interceptor verifies you have an active session
- If you do, the session is cleared and you're logged out
- The error is re-thrown so the original request handler can respond
Why Two Layers?
The preemptive layer catches sessions that are known to be expired locally (e.g., from a keep-alive check). The reactive layer catches cases where the server invalidates your token without the app knowing (e.g., password change on another device, server-side session revocation).
Endpoint Exclusions
Requests That Skip Interceptors
Not all requests should go through the full interceptor chain. Certain requests are explicitly excluded:
| Excluded From | Paths/Conditions | Reason |
|---|---|---|
| Auth Interceptor | /System/Info/Public, /Users/AuthenticateByName, /Users/Public | Public endpoints that must work before login |
| Server URL Interceptor | /assets/*, ./assets/*, URLs starting with http | Static assets and already-complete URLs |
| Server URL Interceptor | Requests with BYPASS_SERVER_URL_INTERCEPTION flag | Server discovery pings and system-level calls |
| Unauthorised Interceptor | Requests with BYPASS_LLAMAFIN_CONNECT_INTERCEPTION flag | P2P peer communication (shouldn't log you out for peer errors) |
| HTTP Error Interceptor | 404 responses on URLs ending with /Lyrics | "No lyrics found" is expected, not an error |
Bypass Flags
The app uses two special flags (HttpContextToken) to skip interceptors for specific requests:
| Flag | Used For |
|---|---|
BYPASS_SERVER_URL_INTERCEPTION | Server discovery pings that shouldn't have the server URL prepended |
BYPASS_LLAMAFIN_CONNECT_INTERCEPTION | P2P communication with other Llamafin devices on your network |
Technical Specifications
Interceptor Chain
| Property | Value |
|---|---|
| Total Interceptors | 6 |
| Registration Pattern | HTTP_INTERCEPTORS multi-provider |
| Request Processing Order | Registration order (1st registered = 1st processed) |
| Response Processing Order | Reverse registration order (6th registered = 1st processed) |
Retry Configuration
| Property | Value |
|---|---|
| Max Retry Attempts | 3 |
| Retry Base Delay | 100ms |
| Backoff Multiplier | 2x per attempt |
| Retry Delays | 100ms, 200ms, 400ms |
| Total Maximum Retry Delay | 700ms |
| Retry Trigger | HTTP status >= 500 or status === 0 (network failure) |
| Excluded from Retry | All 4xx errors (400, 401, 403, 404, etc.) |
Authentication Header
| Component | Value Source | Fallback |
|---|---|---|
| Client name | Settings → Advanced → Player Name | llamafin |
| Device model | Device info (native plugin) | WebBrowser |
| Device ID | Device UUID (native plugin) | unknown-device-id |
| Version | App version (build config) | 1.0.0 |
| Token | Auth state (from login) | (not appended if no session) |
Public Endpoints (No Auth Required)
| Endpoint | Purpose |
|---|---|
/System/Info/Public | Server information (version, name, etc.) |
/Users/AuthenticateByName | Login endpoint |
/Users/Public | Public user list |
Error Toast Messages
| Translation Key | Parameters | Displayed For |
|---|---|---|
errors.HTTP_ERROR_UNKNOWN | None | Network errors, client-side failures |
errors.HTTP_ERROR_CLIENT | status (4xx code) | 400, 404, etc. (excluding 401/403) |
errors.HTTP_ERROR_SERVER | status (5xx code) | 500, 502, 503, etc. |
app.TOAST.SESSION_EXPIRED | None | 401/403 with active session |
Security Considerations
Authentication Security
| Aspect | Implementation |
|---|---|
| Token Storage | Tokens stored in persistent storage (IndexedDB), rehydrated on startup |
| Token Transmission | Sent via X-Emby-Authorization header (not in URL or body) |
| Token Expiry | No automatic token refresh; expired sessions require re-authentication |
| Session Keep-Alive | Server-side session is kept alive by periodic API calls (handled by auth feature, not interceptors) |
Known Limitations
| Limitation | Risk Level | Description |
|---|---|---|
| No token refresh | Medium | Expired tokens require full re-authentication (no silent refresh) |
| No request cancellation on logout | Low | In-flight requests continue after logout, may trigger additional session expiry toasts |
| Hardcoded public paths | Low | Public endpoint paths are hardcoded in the interceptor rather than centralized configuration |
| Substring match for public paths | Low | Auth bypass uses includes() instead of startsWith(), which could theoretically match unintended URLs |
Troubleshooting
Common Issues
| Issue | Likely Cause | Solution |
|---|---|---|
| All API calls fail immediately on app startup | Server URL not configured | Go to Settings → Source and configure your Jellyfin server URL |
| "Session expired" toast appears repeatedly | Token has expired or been revoked | Log out and log back in with your credentials |
| "Server error (500)" toast appears | Jellyfin server is crashing or overloaded | Check your Jellyfin server status, restart if necessary |
| "Unknown network error" toast | No internet connection or server unreachable | Check your network connection, verify server URL is accessible |
| Requests are slow | Server is far away or overloaded | Check request timing in Settings → Advanced → Debugging → App Logs |
Diagnostic Steps
If you're experiencing API errors:
- Check Network Status: Ensure your device has an active internet connection
- Verify Server URL: Confirm the server URL in Settings → Source is correct and accessible from your device
- Check Server Status: Ensure your Jellyfin server is running and accessible
- Review App Logs: Go to Settings → Advanced → Debugging → App Logs to see detailed request timing and error information
- Re-authenticate: Log out and log back in to establish a fresh session with a new token
When to Seek Help
Contact support if:
- You consistently see "Server error" toasts despite a healthy Jellyfin server
- Session expiry occurs immediately after login (may indicate server-side session issues)
- Network errors persist on stable connections (may be a DNS or proxy issue)
Related Documentation
- Authentication - Session management, token lifecycle, login flow
- Security Implementation - Security architecture, threat model, token handling
- Network Management - Network status monitoring, reconnection logic
- WebSocket Communication - Real-time server communication (separate from HTTP)
- Llamafin Connect (P2P) - P2P communication that bypasses interceptors
Last Updated: 2026-04-06 Version: 2.0.0