[Bug]: Custom headers/tokens for Cloudflare Zero Trust are commented out. Patch with fix attached. #1358

Open
opened 2026-04-24 23:57:48 +02:00 by adam · 2 comments
Owner

Originally created by @PandaUnit-TWL on GitHub (Mar 27, 2026).

Patch: Wire custom headers through all three HTTP stacks (fixes Cloudflare Zero Trust / reverse proxy auth)

Re: #254 — Custom header support for Cloudflare tunnels

Summary

The custom headers feature is already implemented in the ABS app codebase — ServerConnectionConfig has a customHeaders field (DeviceClasses.kt line 36–47), CustomHeadersModal.vue exists and works, and the data persists correctly. However, the feature was never fully wired up:

  1. The UI button to access Custom Headers was never exposed on the server connect form
  2. The native Android HTTP layer (ApiHandler.kt) never read customHeaders from the config
  3. The audio player (PlayerNotificationService.kt) never sent custom headers on streaming requests
  4. Several JS-layer requests (/status, /ping, OAuth, token refresh) didn't pass headers through

This meant that even if you manually set custom headers, they were silently dropped on most requests — making the app unusable behind header-authenticated reverse proxies like Cloudflare Zero Trust.

What the patch does

4 files changed, 33 insertions, 15 deletions.

1. components/connection/ServerConnectForm.vue

  • Adds a "Custom Headers" link on the server address entry screen (calls the existing addCustomHeaders() method → opens existing CustomHeadersModal.vue)
  • getServerAddressStatus() now passes this.serverConfig.customHeaders to getRequest()
  • connectToServer() now passes config.customHeaders to pingServerAddress()
  • oauthRequest() now includes this.serverConfig.customHeaders in the CapacitorHttp.get() call

2. plugins/nativeHttp.js

  • request() now spreads serverConnectionConfig.customHeaders into headers for all post-login API calls
  • refreshAccessToken() signature changed from (refreshToken, serverAddress) to (refreshToken, serverConnectionConfig) so custom headers are included on token refresh
  • handleTokenRefresh() updated to pass the full serverConnectionConfig instead of just .address

3. android/app/src/main/java/com/audiobookshelf/app/server/ApiHandler.kt

  • getRequest() — reads customHeaders from config or DeviceManager.serverConnectionConfig, applies via builder.addHeader() loop
  • postRequest() — same treatment (this is the play request fix — /api/items/{id}/play goes through here)
  • patchRequest() — same treatment

4. android/app/src/main/java/com/audiobookshelf/app/player/PlayerNotificationService.kt

  • Direct play path (~line 507): creates directPlayHeaders HashMap with auth + custom headers, calls setDefaultRequestProperties()
  • HLS streaming path (~line 515): creates hlsHeaders HashMap with auth + custom headers, calls setDefaultRequestProperties()

Key architecture note

The ABS app has three separate HTTP stacks that all needed patching:

Layer File Handles
Capacitor/JS nativeHttp.js, ServerConnectForm.vue Login, browsing, API calls from the webview
Native Kotlin API ApiHandler.kt Native API calls including play requests, sync, library fetches
ExoPlayer streaming PlayerNotificationService.kt Actual audio file streaming via OkHttp/DefaultHttpDataSource

All three now pass custom headers consistently.

Known gap

WebSocket (plugins/server.js) does not currently pass extraHeaders to the socket.io connection. This could affect live sync features. If needed, the fix would be adding extraHeaders: DeviceManager.serverConnectionConfig?.customHeaders to the socket options in server.js line 36–41. I haven't hit issues with this yet, but flagging it for completeness.

Patch

diff --git a/android/app/src/main/java/com/audiobookshelf/app/player/PlayerNotificationService.kt b/android/app/src/main/java/com/audiobookshelf/app/player/PlayerNotificationService.kt
index 815cd07..d1aac87 100644
--- a/android/app/src/main/java/com/audiobookshelf/app/player/PlayerNotificationService.kt
+++ b/android/app/src/main/java/com/audiobookshelf/app/player/PlayerNotificationService.kt
@@ -505,6 +505,9 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
         }
 
         dataSourceFactory.setUserAgent(channelId)
+        val directPlayHeaders = hashMapOf("Authorization" to "Bearer ${DeviceManager.token}")
+        DeviceManager.serverConnectionConfig?.customHeaders?.let { directPlayHeaders.putAll(it) }
+        dataSourceFactory.setDefaultRequestProperties(directPlayHeaders)
         mediaSource =
                 ProgressiveMediaSource.Factory(dataSourceFactory, extractorsFactory)
                         .createMediaSource(mediaItems[0])
@@ -512,9 +515,9 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
         AbsLogger.info("PlayerNotificationService", "preparePlayer: Playing HLS stream of item ${currentPlaybackSession?.mediaItemId}.")
         val dataSourceFactory = DefaultHttpDataSource.Factory()
         dataSourceFactory.setUserAgent(channelId)
-        dataSourceFactory.setDefaultRequestProperties(
-                hashMapOf("Authorization" to "Bearer ${DeviceManager.token}")
-        )
+        val hlsHeaders = hashMapOf("Authorization" to "Bearer ${DeviceManager.token}")
+        DeviceManager.serverConnectionConfig?.customHeaders?.let { hlsHeaders.putAll(it) }
+        dataSourceFactory.setDefaultRequestProperties(hlsHeaders)
         mediaSource = HlsMediaSource.Factory(dataSourceFactory).createMediaSource(mediaItems[0])
       }
       mPlayer.setMediaSource(mediaSource)
diff --git a/android/app/src/main/java/com/audiobookshelf/app/server/ApiHandler.kt b/android/app/src/main/java/com/audiobookshelf/app/server/ApiHandler.kt
index d25e744..294822b 100644
--- a/android/app/src/main/java/com/audiobookshelf/app/server/ApiHandler.kt
+++ b/android/app/src/main/java/com/audiobookshelf/app/server/ApiHandler.kt
@@ -56,11 +56,13 @@ class ApiHandler(var ctx:Context) {
   private fun getRequest(endpoint:String, httpClient:OkHttpClient?, config:ServerConnectionConfig?, cb: (JSObject) -> Unit) {
     val address = config?.address ?: DeviceManager.serverAddress
     val token = config?.token ?: DeviceManager.token
+    val customHeaders = config?.customHeaders ?: DeviceManager.serverConnectionConfig?.customHeaders
 
     try {
-      val request = Request.Builder()
+      val builder = Request.Builder()
         .url("${address}$endpoint").addHeader("Authorization", "Bearer $token")
-        .build()
+      customHeaders?.forEach { (key, value) -> builder.addHeader(key, value) }
+      val request = builder.build()
       makeRequest(request, httpClient, cb)
     } catch(e: Exception) {
       e.printStackTrace()
@@ -73,14 +75,16 @@ class ApiHandler(var ctx:Context) {
   private fun postRequest(endpoint:String, payload: JSObject?, config:ServerConnectionConfig?, cb: (JSObject) -> Unit) {
     val address = config?.address ?: DeviceManager.serverAddress
     val token = config?.token ?: DeviceManager.token
+    val customHeaders = config?.customHeaders ?: DeviceManager.serverConnectionConfig?.customHeaders
     val mediaType = "application/json; charset=utf-8".toMediaType()
     val requestBody = payload?.toString()?.toRequestBody(mediaType) ?: EMPTY_REQUEST
     val requestUrl = "${address}$endpoint"
     Log.d(tag, "postRequest to $requestUrl")
     try {
-      val request = Request.Builder().post(requestBody)
+      val builder = Request.Builder().post(requestBody)
         .url(requestUrl).addHeader("Authorization", "Bearer ${token}")
-        .build()
+      customHeaders?.forEach { (key, value) -> builder.addHeader(key, value) }
+      val request = builder.build()
       makeRequest(request, null, cb)
     } catch(e: Exception) {
       e.printStackTrace()
@@ -93,10 +97,12 @@ class ApiHandler(var ctx:Context) {
   private fun patchRequest(endpoint:String, payload: JSObject, cb: (JSObject) -> Unit) {
     val mediaType = "application/json; charset=utf-8".toMediaType()
     val requestBody = payload.toString().toRequestBody(mediaType)
+    val customHeaders = DeviceManager.serverConnectionConfig?.customHeaders
     try {
-      val request = Request.Builder().patch(requestBody)
+      val builder = Request.Builder().patch(requestBody)
         .url("${DeviceManager.serverAddress}$endpoint").addHeader("Authorization", "Bearer ${DeviceManager.token}")
-        .build()
+      customHeaders?.forEach { (key, value) -> builder.addHeader(key, value) }
+      val request = builder.build()
       makeRequest(request, null, cb)
     } catch(e: Exception) {
       e.printStackTrace()
diff --git a/components/connection/ServerConnectForm.vue b/components/connection/ServerConnectForm.vue
index e4abd33..3b1a892 100644
--- a/components/connection/ServerConnectForm.vue
+++ b/components/connection/ServerConnectForm.vue
@@ -42,6 +42,9 @@
           <div class="flex justify-end items-center mt-6">
             <ui-btn :disabled="processing || !networkConnected" type="submit" :padding-x="3" class="h-10">{{ networkConnected ? $strings.ButtonSubmit : $strings.MessageNoNetworkConnection }}</ui-btn>
           </div>
+          <div class="flex justify-center mt-3">
+            <a class="text-fg-muted text-sm cursor-pointer" @click="addCustomHeaders">Custom Headers</a>
+          </div>
         </form>
         <!-- username/password and auth methods -->
         <template v-else>
@@ -298,6 +301,7 @@ export default {
       try {
         const response = await CapacitorHttp.get({
           url: backendEndpoint,
+          headers: this.serverConfig.customHeaders || {},
           disableRedirects: true,
           webFetchExtra: {
             redirect: 'manual'
@@ -454,7 +458,7 @@ export default {
         ...config
       }
       this.showForm = true
-      var success = await this.pingServerAddress(config.address)
+      var success = await this.pingServerAddress(config.address, config.customHeaders)
       this.processing = false
       console.log(`[ServerConnectForm] pingServer result ${success}`)
       if (!success) {
@@ -615,7 +619,7 @@ export default {
      *    HttpResponse.data is {isInit:boolean, language:string, authMethods:string[]}}>
      */
     async getServerAddressStatus(address) {
-      return this.getRequest(`${address}/status`)
+      return this.getRequest(`${address}/status`, this.serverConfig.customHeaders)
     },
     pingServerAddress(address, customHeaders) {
       return this.getRequest(`${address}/ping`, customHeaders)
diff --git a/plugins/nativeHttp.js b/plugins/nativeHttp.js
index 76aa0e0..741172d 100644
--- a/plugins/nativeHttp.js
+++ b/plugins/nativeHttp.js
@@ -27,6 +27,9 @@ export default function ({ store, $db, $socket }, inject) {
         headers = { ...headers, ...options.headers }
         delete options.headers
       }
+      if (serverConnectionConfig?.customHeaders) {
+        headers = { ...headers, ...serverConnectionConfig.customHeaders }
+      }
       console.log(`[nativeHttp] Making ${method} request to ${url}`)
 
       return CapacitorHttp.request({
@@ -76,7 +79,7 @@ export default function ({ store, $db, $socket }, inject) {
         }
 
         // Attempt to refresh the token
-        const newTokens = await this.refreshAccessToken(refreshToken, serverConnectionConfig.address)
+        const newTokens = await this.refreshAccessToken(refreshToken, serverConnectionConfig)
         if (!newTokens?.accessToken) {
           console.error('[nativeHttp] Failed to refresh access token')
           throw new Error('Failed to refresh access token')
@@ -116,11 +119,12 @@ export default function ({ store, $db, $socket }, inject) {
     /**
      * Refreshes the access token using the refresh token
      * @param {string} refreshToken - The refresh token
-     * @param {string} serverAddress - The server address
+     * @param {{ id: string, address: string, customHeaders: Object }} serverConnectionConfig - The server connection config
      * @returns {Promise<Object|null>} - Promise that resolves with new tokens or null
      */
-    async refreshAccessToken(refreshToken, serverAddress) {
+    async refreshAccessToken(refreshToken, serverConnectionConfig) {
       try {
+        const serverAddress = serverConnectionConfig?.address
         if (!serverAddress) {
           throw new Error('No server address available')
         }
@@ -131,7 +135,8 @@ export default function ({ store, $db, $socket }, inject) {
           url: `${serverAddress}/auth/refresh`,
           headers: {
             'Content-Type': 'application/json',
-            'x-refresh-token': refreshToken
+            'x-refresh-token': refreshToken,
+            ...(serverConnectionConfig?.customHeaders || {})
           },
           data: {}
         })

Testing

Tested with a signed release APK against an Audiobookshelf instance behind Cloudflare Zero Trust using service token authentication (CF-Access-Client-Id / CF-Access-Client-Secret headers). Login, library browsing, and audio playback (both direct play and HLS streaming) all work correctly.

Based on master branch.

Copied repo as of late 3/26/26

Originally created by @PandaUnit-TWL on GitHub (Mar 27, 2026). # Patch: Wire custom headers through all three HTTP stacks (fixes Cloudflare Zero Trust / reverse proxy auth) **Re: #254** — Custom header support for Cloudflare tunnels ## Summary The custom headers feature is already implemented in the ABS app codebase — `ServerConnectionConfig` has a `customHeaders` field (`DeviceClasses.kt` line 36–47), `CustomHeadersModal.vue` exists and works, and the data persists correctly. However, the feature was never fully wired up: 1. The UI button to access Custom Headers was never exposed on the server connect form 2. The native Android HTTP layer (`ApiHandler.kt`) never read `customHeaders` from the config 3. The audio player (`PlayerNotificationService.kt`) never sent custom headers on streaming requests 4. Several JS-layer requests (`/status`, `/ping`, OAuth, token refresh) didn't pass headers through This meant that even if you manually set custom headers, they were silently dropped on most requests — making the app unusable behind header-authenticated reverse proxies like Cloudflare Zero Trust. ## What the patch does **4 files changed, 33 insertions, 15 deletions.** ### 1. `components/connection/ServerConnectForm.vue` - Adds a **"Custom Headers"** link on the server address entry screen (calls the existing `addCustomHeaders()` method → opens existing `CustomHeadersModal.vue`) - `getServerAddressStatus()` now passes `this.serverConfig.customHeaders` to `getRequest()` - `connectToServer()` now passes `config.customHeaders` to `pingServerAddress()` - `oauthRequest()` now includes `this.serverConfig.customHeaders` in the `CapacitorHttp.get()` call ### 2. `plugins/nativeHttp.js` - `request()` now spreads `serverConnectionConfig.customHeaders` into headers for all post-login API calls - `refreshAccessToken()` signature changed from `(refreshToken, serverAddress)` to `(refreshToken, serverConnectionConfig)` so custom headers are included on token refresh - `handleTokenRefresh()` updated to pass the full `serverConnectionConfig` instead of just `.address` ### 3. `android/app/src/main/java/com/audiobookshelf/app/server/ApiHandler.kt` - `getRequest()` — reads `customHeaders` from config or `DeviceManager.serverConnectionConfig`, applies via `builder.addHeader()` loop - `postRequest()` — same treatment (this is the play request fix — `/api/items/{id}/play` goes through here) - `patchRequest()` — same treatment ### 4. `android/app/src/main/java/com/audiobookshelf/app/player/PlayerNotificationService.kt` - **Direct play path** (~line 507): creates `directPlayHeaders` HashMap with auth + custom headers, calls `setDefaultRequestProperties()` - **HLS streaming path** (~line 515): creates `hlsHeaders` HashMap with auth + custom headers, calls `setDefaultRequestProperties()` ## Key architecture note The ABS app has **three separate HTTP stacks** that all needed patching: | Layer | File | Handles | |---|---|---| | Capacitor/JS | `nativeHttp.js`, `ServerConnectForm.vue` | Login, browsing, API calls from the webview | | Native Kotlin API | `ApiHandler.kt` | Native API calls including play requests, sync, library fetches | | ExoPlayer streaming | `PlayerNotificationService.kt` | Actual audio file streaming via OkHttp/DefaultHttpDataSource | All three now pass custom headers consistently. ## Known gap **WebSocket** (`plugins/server.js`) does **not** currently pass `extraHeaders` to the socket.io connection. This could affect live sync features. If needed, the fix would be adding `extraHeaders: DeviceManager.serverConnectionConfig?.customHeaders` to the socket options in `server.js` line 36–41. I haven't hit issues with this yet, but flagging it for completeness. ## Patch ```diff diff --git a/android/app/src/main/java/com/audiobookshelf/app/player/PlayerNotificationService.kt b/android/app/src/main/java/com/audiobookshelf/app/player/PlayerNotificationService.kt index 815cd07..d1aac87 100644 --- a/android/app/src/main/java/com/audiobookshelf/app/player/PlayerNotificationService.kt +++ b/android/app/src/main/java/com/audiobookshelf/app/player/PlayerNotificationService.kt @@ -505,6 +505,9 @@ class PlayerNotificationService : MediaBrowserServiceCompat() { } dataSourceFactory.setUserAgent(channelId) + val directPlayHeaders = hashMapOf("Authorization" to "Bearer ${DeviceManager.token}") + DeviceManager.serverConnectionConfig?.customHeaders?.let { directPlayHeaders.putAll(it) } + dataSourceFactory.setDefaultRequestProperties(directPlayHeaders) mediaSource = ProgressiveMediaSource.Factory(dataSourceFactory, extractorsFactory) .createMediaSource(mediaItems[0]) @@ -512,9 +515,9 @@ class PlayerNotificationService : MediaBrowserServiceCompat() { AbsLogger.info("PlayerNotificationService", "preparePlayer: Playing HLS stream of item ${currentPlaybackSession?.mediaItemId}.") val dataSourceFactory = DefaultHttpDataSource.Factory() dataSourceFactory.setUserAgent(channelId) - dataSourceFactory.setDefaultRequestProperties( - hashMapOf("Authorization" to "Bearer ${DeviceManager.token}") - ) + val hlsHeaders = hashMapOf("Authorization" to "Bearer ${DeviceManager.token}") + DeviceManager.serverConnectionConfig?.customHeaders?.let { hlsHeaders.putAll(it) } + dataSourceFactory.setDefaultRequestProperties(hlsHeaders) mediaSource = HlsMediaSource.Factory(dataSourceFactory).createMediaSource(mediaItems[0]) } mPlayer.setMediaSource(mediaSource) diff --git a/android/app/src/main/java/com/audiobookshelf/app/server/ApiHandler.kt b/android/app/src/main/java/com/audiobookshelf/app/server/ApiHandler.kt index d25e744..294822b 100644 --- a/android/app/src/main/java/com/audiobookshelf/app/server/ApiHandler.kt +++ b/android/app/src/main/java/com/audiobookshelf/app/server/ApiHandler.kt @@ -56,11 +56,13 @@ class ApiHandler(var ctx:Context) { private fun getRequest(endpoint:String, httpClient:OkHttpClient?, config:ServerConnectionConfig?, cb: (JSObject) -> Unit) { val address = config?.address ?: DeviceManager.serverAddress val token = config?.token ?: DeviceManager.token + val customHeaders = config?.customHeaders ?: DeviceManager.serverConnectionConfig?.customHeaders try { - val request = Request.Builder() + val builder = Request.Builder() .url("${address}$endpoint").addHeader("Authorization", "Bearer $token") - .build() + customHeaders?.forEach { (key, value) -> builder.addHeader(key, value) } + val request = builder.build() makeRequest(request, httpClient, cb) } catch(e: Exception) { e.printStackTrace() @@ -73,14 +75,16 @@ class ApiHandler(var ctx:Context) { private fun postRequest(endpoint:String, payload: JSObject?, config:ServerConnectionConfig?, cb: (JSObject) -> Unit) { val address = config?.address ?: DeviceManager.serverAddress val token = config?.token ?: DeviceManager.token + val customHeaders = config?.customHeaders ?: DeviceManager.serverConnectionConfig?.customHeaders val mediaType = "application/json; charset=utf-8".toMediaType() val requestBody = payload?.toString()?.toRequestBody(mediaType) ?: EMPTY_REQUEST val requestUrl = "${address}$endpoint" Log.d(tag, "postRequest to $requestUrl") try { - val request = Request.Builder().post(requestBody) + val builder = Request.Builder().post(requestBody) .url(requestUrl).addHeader("Authorization", "Bearer ${token}") - .build() + customHeaders?.forEach { (key, value) -> builder.addHeader(key, value) } + val request = builder.build() makeRequest(request, null, cb) } catch(e: Exception) { e.printStackTrace() @@ -93,10 +97,12 @@ class ApiHandler(var ctx:Context) { private fun patchRequest(endpoint:String, payload: JSObject, cb: (JSObject) -> Unit) { val mediaType = "application/json; charset=utf-8".toMediaType() val requestBody = payload.toString().toRequestBody(mediaType) + val customHeaders = DeviceManager.serverConnectionConfig?.customHeaders try { - val request = Request.Builder().patch(requestBody) + val builder = Request.Builder().patch(requestBody) .url("${DeviceManager.serverAddress}$endpoint").addHeader("Authorization", "Bearer ${DeviceManager.token}") - .build() + customHeaders?.forEach { (key, value) -> builder.addHeader(key, value) } + val request = builder.build() makeRequest(request, null, cb) } catch(e: Exception) { e.printStackTrace() diff --git a/components/connection/ServerConnectForm.vue b/components/connection/ServerConnectForm.vue index e4abd33..3b1a892 100644 --- a/components/connection/ServerConnectForm.vue +++ b/components/connection/ServerConnectForm.vue @@ -42,6 +42,9 @@ <div class="flex justify-end items-center mt-6"> <ui-btn :disabled="processing || !networkConnected" type="submit" :padding-x="3" class="h-10">{{ networkConnected ? $strings.ButtonSubmit : $strings.MessageNoNetworkConnection }}</ui-btn> </div> + <div class="flex justify-center mt-3"> + <a class="text-fg-muted text-sm cursor-pointer" @click="addCustomHeaders">Custom Headers</a> + </div> </form> <!-- username/password and auth methods --> <template v-else> @@ -298,6 +301,7 @@ export default { try { const response = await CapacitorHttp.get({ url: backendEndpoint, + headers: this.serverConfig.customHeaders || {}, disableRedirects: true, webFetchExtra: { redirect: 'manual' @@ -454,7 +458,7 @@ export default { ...config } this.showForm = true - var success = await this.pingServerAddress(config.address) + var success = await this.pingServerAddress(config.address, config.customHeaders) this.processing = false console.log(`[ServerConnectForm] pingServer result ${success}`) if (!success) { @@ -615,7 +619,7 @@ export default { * HttpResponse.data is {isInit:boolean, language:string, authMethods:string[]}}> */ async getServerAddressStatus(address) { - return this.getRequest(`${address}/status`) + return this.getRequest(`${address}/status`, this.serverConfig.customHeaders) }, pingServerAddress(address, customHeaders) { return this.getRequest(`${address}/ping`, customHeaders) diff --git a/plugins/nativeHttp.js b/plugins/nativeHttp.js index 76aa0e0..741172d 100644 --- a/plugins/nativeHttp.js +++ b/plugins/nativeHttp.js @@ -27,6 +27,9 @@ export default function ({ store, $db, $socket }, inject) { headers = { ...headers, ...options.headers } delete options.headers } + if (serverConnectionConfig?.customHeaders) { + headers = { ...headers, ...serverConnectionConfig.customHeaders } + } console.log(`[nativeHttp] Making ${method} request to ${url}`) return CapacitorHttp.request({ @@ -76,7 +79,7 @@ export default function ({ store, $db, $socket }, inject) { } // Attempt to refresh the token - const newTokens = await this.refreshAccessToken(refreshToken, serverConnectionConfig.address) + const newTokens = await this.refreshAccessToken(refreshToken, serverConnectionConfig) if (!newTokens?.accessToken) { console.error('[nativeHttp] Failed to refresh access token') throw new Error('Failed to refresh access token') @@ -116,11 +119,12 @@ export default function ({ store, $db, $socket }, inject) { /** * Refreshes the access token using the refresh token * @param {string} refreshToken - The refresh token - * @param {string} serverAddress - The server address + * @param {{ id: string, address: string, customHeaders: Object }} serverConnectionConfig - The server connection config * @returns {Promise<Object|null>} - Promise that resolves with new tokens or null */ - async refreshAccessToken(refreshToken, serverAddress) { + async refreshAccessToken(refreshToken, serverConnectionConfig) { try { + const serverAddress = serverConnectionConfig?.address if (!serverAddress) { throw new Error('No server address available') } @@ -131,7 +135,8 @@ export default function ({ store, $db, $socket }, inject) { url: `${serverAddress}/auth/refresh`, headers: { 'Content-Type': 'application/json', - 'x-refresh-token': refreshToken + 'x-refresh-token': refreshToken, + ...(serverConnectionConfig?.customHeaders || {}) }, data: {} }) ``` ## Testing Tested with a signed release APK against an Audiobookshelf instance behind Cloudflare Zero Trust using service token authentication (`CF-Access-Client-Id` / `CF-Access-Client-Secret` headers). Login, library browsing, and audio playback (both direct play and HLS streaming) all work correctly. Based on `master` branch. Copied repo as of late 3/26/26
adam added the bug label 2026-04-24 23:57:48 +02:00
Author
Owner

@PandaUnit-TWL commented on GitHub (Mar 27, 2026):

abs-custom-headers-full.patch

@PandaUnit-TWL commented on GitHub (Mar 27, 2026): [abs-custom-headers-full.patch](https://github.com/user-attachments/files/26296885/abs-custom-headers-full.patch)
Author
Owner

@nichwall commented on GitHub (Mar 28, 2026):

We are not reviewing AI generated PRs or issues at this time due to an increasing number of low quality PRs and limited reviewer time.

@nichwall commented on GitHub (Mar 28, 2026): We are not reviewing AI generated PRs or issues at this time due to an increasing number of low quality PRs and limited reviewer time.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: starred/audiobookshelf-app#1358