Completes the custom HTTP headers implementation for the Android app. The app already had UI scaffolding (CustomHeadersModal.vue) and a customHeaders field on ServerConnectionConfig, but headers were never actually sent with requests. This PR wires them through every HTTP path so the app works behind reverse proxies that require authentication headers (Cloudflare Access, Authelia, Authentik, etc.).
Duplicate token refresh code path (Vuex store action) — same logout bug
components/readers/PdfReader.vue
Add custom headers to pdfjs httpHeaders config
PDF/ebook viewer has its own HTTP layer that doesn't go through axios or nativeHttp
plugins/server.js
Accept customHeaders in connect(), pass as Socket.IO extraHeaders
WebSocket was connecting without auth headers — blocked by proxy
layouts/default.vue
Pass customHeaders when reconnecting socket on app resume
Auto-reconnect used old connect() signature without headers
Commit 3: UX fix (fix: show edit form immediately without re-validating server connection)
File
Change
Why
ServerConnectForm.vue
editServerConfig() now shows the form directly instead of calling submit(true) first
Previously, editing a saved server config required the server to be reachable. If the server was down or the address changed, you couldn't edit the config. Now matches newServerConfigClick() behavior.
Commit 4: Ebook reader fix (fix: enable CapacitorHttp to route ebook requests through native HTTP)
The ebook readers (epub, comic, mobi, PDF) use axios/XHR for binary data fetches (responseType: 'arraybuffer'/'blob'). With CapacitorHttp.enabled: false, these XHR requests execute in the WebView and are subject to CORS enforcement. Behind reverse proxies like Cloudflare Access, the CORS preflight OPTIONS request is blocked because preflight requests cannot carry custom authentication headers per the CORS spec.
File
Change
Why
capacitor.config.json
Set CapacitorHttp.enabled: true
Patches global XMLHttpRequest/fetch to route through native HTTP, bypassing CORS while preserving binary response types
android/.../capacitor.config.json
Same change in Android assets copy
Android build reads from this copy
Why this is safe now: This was previously disabled because Capacitor 4's native HTTP broke binary responses (returning base64 strings instead of ArrayBuffer/Blob). Capacitor 5.7+ (PR #6818) fixed this for GET requests by using a proxy URL approach — the native layer handles the HTTP request but the WebView's real XHR processes the response, preserving proper binary types. The app is on Capacitor 7.
Known limitations
iOS: No custom header support at all — ServerConnectionConfig.swift doesn't have a customHeaders property. Out of scope for this PR.
ExoPlayer HLS streaming: Only sends Authorization header, not custom headers. Direct play uses token-in-URL so it works regardless.
Cover images via <img> tags: Browser limitation — <img> elements can't carry custom headers. Works if server doesn't require auth for cover endpoints.
How have you tested this?
Configured Cloudflare Access with Service Token authentication (CF-Access-Client-Id + CF-Access-Client-Secret) in front of audiobookshelf server
Added headers via the Custom Headers modal in the app
Verified: server ping, login, library browsing, audiobook streaming, downloads (both internal and external storage), ebook/PDF reading, real-time sync via Socket.IO
Verified token refresh works without triggering logout
Verified headers persist across app restarts
Verified headers update correctly when editing an existing server connection
Verified no regression in audio playback, navigation, or socket connections after enabling CapacitorHttp
Screenshots
🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.
## 📋 Pull Request Information
**Original PR:** https://github.com/advplyr/audiobookshelf-app/pull/1768
**Author:** [@yaker-gh](https://github.com/yaker-gh)
**Created:** 12/30/2025
**Status:** 🔄 Open
**Base:** `master` ← **Head:** `add-custom-headers`
---
### 📝 Commits (5)
- [`1345676`](https://github.com/advplyr/audiobookshelf-app/commit/1345676bb9fb6b37e1f3f2586f0197c1f80ecb02) feat(android): support custom HTTP headers for reverse proxy auth
- [`e48043f`](https://github.com/advplyr/audiobookshelf-app/commit/e48043f0eec0fa12063f9c88507df3df599d4dd7) fix: add CF Access custom headers to all HTTP paths
- [`b863f21`](https://github.com/advplyr/audiobookshelf-app/commit/b863f21e7019d8538190e179ac4408c7a7242011) fix: show edit form immediately without re-validating server connection
- [`8e6fee1`](https://github.com/advplyr/audiobookshelf-app/commit/8e6fee10637608dfb95fd23b98df292430545b1c) fix: enable CapacitorHttp to route ebook requests through native HTTP
- [`1a3ff3a`](https://github.com/advplyr/audiobookshelf-app/commit/1a3ff3a52d8c70277c31b5a8e80f0a58db321cce) Merge branch 'advplyr:master' into add-custom-headers
### 📊 Changes
**16 files changed** (+133 additions, -47 deletions)
<details>
<summary>View changed files</summary>
📝 `android/app/src/main/assets/capacitor.config.json` (+1 -1)
📝 `android/app/src/main/java/com/audiobookshelf/app/data/PlaybackSession.kt` (+14 -10)
📝 `android/app/src/main/java/com/audiobookshelf/app/device/DeviceManager.kt` (+2 -0)
📝 `android/app/src/main/java/com/audiobookshelf/app/managers/DownloadItemManager.kt` (+1 -1)
📝 `android/app/src/main/java/com/audiobookshelf/app/managers/InternalDownloadManager.kt` (+9 -3)
📝 `android/app/src/main/java/com/audiobookshelf/app/models/DownloadItemPart.kt` (+5 -0)
📝 `android/app/src/main/java/com/audiobookshelf/app/plugins/AbsDatabase.kt` (+8 -1)
📝 `android/app/src/main/java/com/audiobookshelf/app/server/ApiHandler.kt` (+26 -9)
📝 `capacitor.config.json` (+1 -1)
📝 `components/connection/ServerConnectForm.vue` (+18 -6)
📝 `components/readers/PdfReader.vue` (+9 -3)
📝 `layouts/default.vue` (+1 -1)
📝 `plugins/axios.js` (+7 -0)
📝 `plugins/nativeHttp.js` (+16 -6)
📝 `plugins/server.js` (+5 -1)
📝 `store/user.js` (+10 -4)
</details>
### 📄 Description
## Brief summary
Completes the custom HTTP headers implementation for the Android app. The app already had UI scaffolding (`CustomHeadersModal.vue`) and a `customHeaders` field on `ServerConnectionConfig`, but headers were never actually sent with requests. This PR wires them through every HTTP path so the app works behind reverse proxies that require authentication headers (Cloudflare Access, Authelia, Authentik, etc.).
## Which issue is fixed?
Related to discussion: https://github.com/advplyr/audiobookshelf-app/discussions/1313
Closes: https://github.com/advplyr/audiobookshelf-app/issues/254
## Pull Request Type
- **Platform:** Android only
- **Scope:** Frontend (Vue/JS) and Backend (Kotlin)
## In-depth Description
### Commit 1: Core plumbing (`feat(android): support custom HTTP headers for reverse proxy auth`)
**Android (Kotlin):**
| File | Change | Why |
|------|--------|-----|
| `ApiHandler.kt` | Inject custom headers into `getRequest()`, `postRequest()`, `patchRequest()` | All API calls funnel through these methods — single injection point covers all endpoints |
| `DeviceManager.kt` | Add `customHeaders` getter property | Exposes headers from current `ServerConnectionConfig` to all Kotlin code |
| `AbsDatabase.kt` | Sync `customHeaders` when updating *existing* server configs | Previously only set on new configs — editing headers on an existing server had no effect |
| `DownloadItemPart.kt` | Add custom headers to Android `DownloadManager` requests | External-storage downloads use the system DownloadManager, not OkHttp |
| `InternalDownloadManager.kt` | Accept + apply custom headers to OkHttp download requests | Internal-storage downloads go through a separate OkHttp client |
| `DownloadItemManager.kt` | Pass `DeviceManager.customHeaders` to `InternalDownloadManager` | Wiring between header source and download pipeline |
| `PlaybackSession.kt` | try-catch around local cover bitmap decoding | Prevents crash when cover image decode fails (encountered during testing) |
**Frontend (Vue/JS):**
| File | Change | Why |
|------|--------|-----|
| `ServerConnectForm.vue` | Add "Custom Headers" button to UI; pass headers to ping, status check, and socket connect | Users need to configure headers AND they need to be used during initial connection |
| `plugins/nativeHttp.js` | Add custom headers to all `CapacitorHttp` requests | JS-layer native HTTP calls were missing headers entirely |
### Commit 2: Fix remaining HTTP paths (`fix: add CF Access custom headers to all HTTP paths`)
During real-world testing behind Cloudflare Access, several HTTP paths were found to be missing custom headers, causing:
- **Constant logouts** — token refresh requests were blocked by CF Access, so the app thought the token was invalid
- **Broken ebook/PDF rendering** — pdfjs fetches content through its own HTTP layer without custom headers
- **Broken real-time sync** — Socket.IO WebSocket connections were blocked by CF Access
| File | Change | Why |
|------|--------|-----|
| `plugins/axios.js` | Request interceptor injects custom headers into all axios requests | Axios is used alongside nativeHttp for many API calls — was completely missing headers |
| `plugins/nativeHttp.js` | Add headers to `refreshAccessToken()`; pass `serverConnectionConfig` through call chain | Token refresh hitting `/auth/refresh` without CF headers → blocked → forced logout |
| `store/user.js` | Add headers to Vuex `refreshToken` action | Duplicate token refresh code path (Vuex store action) — same logout bug |
| `components/readers/PdfReader.vue` | Add custom headers to pdfjs `httpHeaders` config | PDF/ebook viewer has its own HTTP layer that doesn't go through axios or nativeHttp |
| `plugins/server.js` | Accept `customHeaders` in `connect()`, pass as Socket.IO `extraHeaders` | WebSocket was connecting without auth headers — blocked by proxy |
| `layouts/default.vue` | Pass `customHeaders` when reconnecting socket on app resume | Auto-reconnect used old `connect()` signature without headers |
### Commit 3: UX fix (`fix: show edit form immediately without re-validating server connection`)
| File | Change | Why |
|------|--------|-----|
| `ServerConnectForm.vue` | `editServerConfig()` now shows the form directly instead of calling `submit(true)` first | Previously, editing a saved server config required the server to be reachable. If the server was down or the address changed, you couldn't edit the config. Now matches `newServerConfigClick()` behavior. |
### Commit 4: Ebook reader fix (`fix: enable CapacitorHttp to route ebook requests through native HTTP`)
The ebook readers (epub, comic, mobi, PDF) use axios/XHR for binary data fetches (`responseType: 'arraybuffer'`/`'blob'`). With `CapacitorHttp.enabled: false`, these XHR requests execute in the WebView and are subject to CORS enforcement. Behind reverse proxies like Cloudflare Access, the CORS preflight `OPTIONS` request is blocked because preflight requests cannot carry custom authentication headers per the CORS spec.
| File | Change | Why |
|------|--------|-----|
| `capacitor.config.json` | Set `CapacitorHttp.enabled: true` | Patches global `XMLHttpRequest`/`fetch` to route through native HTTP, bypassing CORS while preserving binary response types |
| `android/.../capacitor.config.json` | Same change in Android assets copy | Android build reads from this copy |
**Why this is safe now:** This was previously disabled because Capacitor 4's native HTTP broke binary responses (returning base64 strings instead of ArrayBuffer/Blob). Capacitor 5.7+ (PR [#6818](https://github.com/ionic-team/capacitor/pull/6818)) fixed this for GET requests by using a proxy URL approach — the native layer handles the HTTP request but the WebView's real XHR processes the response, preserving proper binary types. The app is on Capacitor 7.
### Known limitations
- **iOS:** No custom header support at all — `ServerConnectionConfig.swift` doesn't have a `customHeaders` property. Out of scope for this PR.
- **ExoPlayer HLS streaming:** Only sends `Authorization` header, not custom headers. Direct play uses token-in-URL so it works regardless.
- **Cover images via `<img>` tags:** Browser limitation — `<img>` elements can't carry custom headers. Works if server doesn't require auth for cover endpoints.
## How have you tested this?
- Configured Cloudflare Access with Service Token authentication (`CF-Access-Client-Id` + `CF-Access-Client-Secret`) in front of audiobookshelf server
- Added headers via the Custom Headers modal in the app
- Verified: server ping, login, library browsing, audiobook streaming, downloads (both internal and external storage), ebook/PDF reading, real-time sync via Socket.IO
- Verified token refresh works without triggering logout
- Verified headers persist across app restarts
- Verified headers update correctly when editing an existing server connection
- Verified ebook readers (epub, comic, mobi, PDF) load correctly behind CF Access after enabling CapacitorHttp
- Verified no regression in audio playback, navigation, or socket connections after enabling CapacitorHttp
## Screenshots
<img width="864" height="1939" alt="Screenshot_20251229-231947" src="https://github.com/user-attachments/assets/bd1325e0-90c5-47b5-8b23-6d965992adad" />
<img width="864" height="1939" alt="Screenshot_20251229-231904" src="https://github.com/user-attachments/assets/1f8dba01-8486-453d-b8fc-7546f046e9cc" />
<img width="864" height="1939" alt="Screenshot_20251229-231856" src="https://github.com/user-attachments/assets/331b04ca-7df9-4a06-a1ed-1516c5cd8802" />
<img width="864" height="1939" alt="Screenshot_20251229-231816" src="https://github.com/user-attachments/assets/830ca520-0b5c-4e2c-be39-4b6abcf50763" />
---
<sub>🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.</sub>
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
📋 Pull Request Information
Original PR: https://github.com/advplyr/audiobookshelf-app/pull/1768
Author: @yaker-gh
Created: 12/30/2025
Status: 🔄 Open
Base:
master← Head:add-custom-headers📝 Commits (5)
1345676feat(android): support custom HTTP headers for reverse proxy authe48043ffix: add CF Access custom headers to all HTTP pathsb863f21fix: show edit form immediately without re-validating server connection8e6fee1fix: enable CapacitorHttp to route ebook requests through native HTTP1a3ff3aMerge branch 'advplyr:master' into add-custom-headers📊 Changes
16 files changed (+133 additions, -47 deletions)
View changed files
📝
android/app/src/main/assets/capacitor.config.json(+1 -1)📝
android/app/src/main/java/com/audiobookshelf/app/data/PlaybackSession.kt(+14 -10)📝
android/app/src/main/java/com/audiobookshelf/app/device/DeviceManager.kt(+2 -0)📝
android/app/src/main/java/com/audiobookshelf/app/managers/DownloadItemManager.kt(+1 -1)📝
android/app/src/main/java/com/audiobookshelf/app/managers/InternalDownloadManager.kt(+9 -3)📝
android/app/src/main/java/com/audiobookshelf/app/models/DownloadItemPart.kt(+5 -0)📝
android/app/src/main/java/com/audiobookshelf/app/plugins/AbsDatabase.kt(+8 -1)📝
android/app/src/main/java/com/audiobookshelf/app/server/ApiHandler.kt(+26 -9)📝
capacitor.config.json(+1 -1)📝
components/connection/ServerConnectForm.vue(+18 -6)📝
components/readers/PdfReader.vue(+9 -3)📝
layouts/default.vue(+1 -1)📝
plugins/axios.js(+7 -0)📝
plugins/nativeHttp.js(+16 -6)📝
plugins/server.js(+5 -1)📝
store/user.js(+10 -4)📄 Description
Brief summary
Completes the custom HTTP headers implementation for the Android app. The app already had UI scaffolding (
CustomHeadersModal.vue) and acustomHeadersfield onServerConnectionConfig, but headers were never actually sent with requests. This PR wires them through every HTTP path so the app works behind reverse proxies that require authentication headers (Cloudflare Access, Authelia, Authentik, etc.).Which issue is fixed?
Related to discussion: https://github.com/advplyr/audiobookshelf-app/discussions/1313
Closes: https://github.com/advplyr/audiobookshelf-app/issues/254
Pull Request Type
In-depth Description
Commit 1: Core plumbing (
feat(android): support custom HTTP headers for reverse proxy auth)Android (Kotlin):
ApiHandler.ktgetRequest(),postRequest(),patchRequest()DeviceManager.ktcustomHeadersgetter propertyServerConnectionConfigto all Kotlin codeAbsDatabase.ktcustomHeaderswhen updating existing server configsDownloadItemPart.ktDownloadManagerrequestsInternalDownloadManager.ktDownloadItemManager.ktDeviceManager.customHeaderstoInternalDownloadManagerPlaybackSession.ktFrontend (Vue/JS):
ServerConnectForm.vueplugins/nativeHttp.jsCapacitorHttprequestsCommit 2: Fix remaining HTTP paths (
fix: add CF Access custom headers to all HTTP paths)During real-world testing behind Cloudflare Access, several HTTP paths were found to be missing custom headers, causing:
plugins/axios.jsplugins/nativeHttp.jsrefreshAccessToken(); passserverConnectionConfigthrough call chain/auth/refreshwithout CF headers → blocked → forced logoutstore/user.jsrefreshTokenactioncomponents/readers/PdfReader.vuehttpHeadersconfigplugins/server.jscustomHeadersinconnect(), pass as Socket.IOextraHeaderslayouts/default.vuecustomHeaderswhen reconnecting socket on app resumeconnect()signature without headersCommit 3: UX fix (
fix: show edit form immediately without re-validating server connection)ServerConnectForm.vueeditServerConfig()now shows the form directly instead of callingsubmit(true)firstnewServerConfigClick()behavior.Commit 4: Ebook reader fix (
fix: enable CapacitorHttp to route ebook requests through native HTTP)The ebook readers (epub, comic, mobi, PDF) use axios/XHR for binary data fetches (
responseType: 'arraybuffer'/'blob'). WithCapacitorHttp.enabled: false, these XHR requests execute in the WebView and are subject to CORS enforcement. Behind reverse proxies like Cloudflare Access, the CORS preflightOPTIONSrequest is blocked because preflight requests cannot carry custom authentication headers per the CORS spec.capacitor.config.jsonCapacitorHttp.enabled: trueXMLHttpRequest/fetchto route through native HTTP, bypassing CORS while preserving binary response typesandroid/.../capacitor.config.jsonWhy this is safe now: This was previously disabled because Capacitor 4's native HTTP broke binary responses (returning base64 strings instead of ArrayBuffer/Blob). Capacitor 5.7+ (PR #6818) fixed this for GET requests by using a proxy URL approach — the native layer handles the HTTP request but the WebView's real XHR processes the response, preserving proper binary types. The app is on Capacitor 7.
Known limitations
ServerConnectionConfig.swiftdoesn't have acustomHeadersproperty. Out of scope for this PR.Authorizationheader, not custom headers. Direct play uses token-in-URL so it works regardless.<img>tags: Browser limitation —<img>elements can't carry custom headers. Works if server doesn't require auth for cover endpoints.How have you tested this?
CF-Access-Client-Id+CF-Access-Client-Secret) in front of audiobookshelf serverScreenshots
🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.