maplibre-react-native
maplibre-react-native copied to clipboard
iOS (TestFlight): addCustomHeader('Authorization', 'Bearer …') is not attached to vector tile requests in release builds; works in local dev
Describe and reproduce the Bug
iOS (TestFlight): addCustomHeader('Authorization', 'Bearer …') not attached to vector tile requests in release; works in local dev
Summary
On iOS, while developing locally (npx expo run:ios) the map loads correctly and my API receives the Authorization: Bearer <token> header on all vector tile requests.
After distributing the same code via TestFlight (release build), the exact same style stops receiving the token header on the server side, and vector tiles fail (401/403). No style/code changes between dev and TF.
This only impacts tile requests (the …/{z}/{x}/{y}.pbf requests). Other fetches I do in-app (Axios pre-warm) include the header fine. It looks like the header set via @maplibre/maplibre-react-native is either not attached or gets lost in release.
Environment
- @maplibre/maplibre-react-native: 10.2.0
- React Native: 0.81.4
- Expo: 54.0.10 (managed workflow, prebuild present due to
expo run:ios), built with EAS - iOS: 26 (reproduced on iPhone 14 Pro / iOS 26)
- Distribution: TestFlight (App Store Connect), Release build (no code changes from local)
- Server: HTTPS (valid cert), custom Koop/ArcGIS backend expecting
Authorization: Bearer <jwt>
App Transport Security is satisfied (valid CA cert). The server is receiving requests; the only difference is the missing Authorization header on tile requests in TF.
Minimal style (vector sources)
{
"version": 8,
"sources": {
"countries": {
"type": "vector",
"tiles": [
"https://<domain>:8446/boundaries/rest/services/IPCERT/countries/VectorTileServer/{z}/{x}/{y}.pbf"
],
"minzoom": 0,
"maxzoom": 6
},
"provinces": {
"type": "vector",
"tiles": [
"https://<domain>:8446/boundaries/rest/services/IPCERT/provinces/VectorTileServer/{z}/{x}/{y}.pbf"
],
"minzoom": 4.5,
"maxzoom": 9
},
"municipalities": {
"type": "vector",
"tiles": [
"https://<domain>:8446/boundaries/rest/services/IPCERT/municipalities/VectorTileServer/{z}/{x}/{y}.pbf"
],
"minzoom": 7.5
}
},
"layers": [
{ "id": "countries", "type": "fill", "source": "countries" },
{ "id": "provinces", "type": "fill", "source": "provinces" },
{
"id": "municipalities",
"type": "fill",
"source": "municipalities"
}
]
}
Code to set the header (minimal)
// useMapAuthHeader.ts
import { useEffect, useState } from "react";
import { AppState } from "react-native";
import {
addCustomHeader,
removeCustomHeader,
} from "@maplibre/maplibre-react-native";
import { useAuth } from "@/Context/AuthContext";
const HEADER_NAME = "Authorization";
export function useMapAuthHeader() {
const { token } = useAuth();
const [isAuthReady, setIsAuthReady] = useState(false);
useEffect(() => {
try {
removeCustomHeader(HEADER_NAME);
if (token) {
addCustomHeader(HEADER_NAME, `Bearer ${token}`);
setIsAuthReady(true);
}
} catch {}
return () => {
try {
removeCustomHeader(HEADER_NAME);
} catch {}
};
}, [token]);
// Re-apply on foreground (iOS can recreate sessions)
useEffect(() => {
const sub = AppState.addEventListener("change", (s) => {
if (s === "active" && token) {
try {
removeCustomHeader(HEADER_NAME);
addCustomHeader(HEADER_NAME, `Bearer ${token}`);
} catch {}
}
});
return () => sub.remove();
}, [token]);
return { isAuthReady };
}
Token/header readiness guards
I gate map rendering until the header is applied, the style is ready and Koop has been prewarmed; in practice this delays any MapView network until after auth header setup.
// Pseudocode of the guards I have around MapView mount
const { combinedMapStyle, isMapStyleReady } = useMapStyle();
const { isAuthReady } = useMapAuthHeader();
const { prewarmDone } = useKoopPrewarm(isMapStyleReady, combinedMapStyle);
if (!isMapStyleReady || !prewarmDone || !isAuthReady) {
return <Loader />; // avoid mounting MapView until ready
}
return <MapView mapStyle={combinedMapStyle} ... />;
Even with these guards (and verifying the header was applied before mounting), TestFlight builds still produce tile requests without the Authorization header on the server side.
Prewarm behavior (works with header)
To reduce backend pressure I “prewarm” Koop by fetching one tile per source via Axios with the same token. These prewarm calls reach the server with Authorization: Bearer … both locally and in TestFlight, and succeed (200/204 no content).
Only MapLibre tile requests in TestFlight are missing the header.
Snippet (simplified):
await api.get<ArrayBuffer>(tileUrl, {
responseType: "arraybuffer",
transformResponse: [],
// axios has Authorization header through my API client interceptors
});
Runtime style rewriting (query params)
At runtime I rewrite source URLs to append server-side filters (e.g., ?lsPaisIds=[123], ?lsProvinciaIds=[49], ?lsMunicipioIds=[6709]). This affects only the query string, not the host.
This rewrite is unrelated to the header problem: locally (dev) the server still sees Authorization regardless of the additional query params; in TestFlight the header is missing even if I disable rewriting entirely.
iOS native setup (AppDelegate & bridging header)
I also have the following in AppDelegate.swift (Expo prebuild, Swift):
import Expo
import React
import ReactAppDependencyProvider
@UIApplicationMain
public class AppDelegate: ExpoAppDelegate {
var window: UIWindow?
var reactNativeDelegate: ExpoReactNativeFactoryDelegate?
var reactNativeFactory: RCTReactNativeFactory?
public override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil
) -> Bool {
let delegate = ReactNativeDelegate()
let factory = ExpoReactNativeFactory(delegate: delegate)
delegate.dependencyProvider = RCTAppDependencyProvider()
reactNativeDelegate = delegate
reactNativeFactory = factory
bindReactNativeFactory(factory)
#if os(iOS) || os(tvOS)
window = UIWindow(frame: UIScreen.main.bounds)
// Initialize MapLibre custom headers (restored)
MLRNCustomHeaders().initHeaders()
MLRNCustomHeaders().addHeader("sECrEt", forHeaderName: "Authorization")
factory.startReactNative(
withModuleName: "main",
in: window,
launchOptions: launchOptions)
#endif
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
// Linking API
public override func application(
_ app: UIApplication,
open url: URL,
options: [UIApplication.OpenURLOptionsKey: Any] = [:]
) -> Bool {
return super.application(app, open: url, options: options) || RCTLinkingManager.application(app, open: url, options: options)
}
// Universal Links
public override func application(
_ application: UIApplication,
continue userActivity: NSUserActivity,
restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void
) -> Bool {
let result = RCTLinkingManager.application(application, continue: userActivity, restorationHandler: restorationHandler)
return super.application(application, continue: userActivity, restorationHandler: restorationHandler) || result
}
}
class ReactNativeDelegate: ExpoReactNativeFactoryDelegate {
// Extension point for config-plugins
override func sourceURL(for bridge: RCTBridge) -> URL? {
// needed to return the correct URL for expo-dev-client.
bridge.bundleURL ?? bundleURL()
}
override func bundleURL() -> URL? {
#if DEBUG
return RCTBundleURLProvider.sharedSettings().jsBundleURL(forBundleRoot: ".expo/.virtual-metro-entry")
#else
return Bundle.main.url(forResource: "main", withExtension: "jsbundle")
#endif
}
}
Specifically these lines are intended to ensure custom headers are available from app start:
MLRNCustomHeaders().initHeaders()
MLRNCustomHeaders().addHeader("sECrEt", forHeaderName: "Authorization")
I do not import MLRNCustomHeaders at the top of AppDelegate.swift because it failed to compile if imported directly. Instead, I created a bridging header GlobalIPCert-Bridging-Header.h with:
//
// Use this file to import your target's public headers that you would like to expose to Swift.
//
#import "MLRNCustomHeaders.h"
With this setup the app compiled and used to work; recently, without changing this native code, TestFlight builds stopped sending the Authorization header on MapLibre tile requests while local dev still works.
Note on v11 alpha (upgrade attempt)
Some time ago I tried migrating to @maplibre/maplibre-react-native v11 alpha, but I could not even compile because the calls:
MLRNCustomHeaders().initHeaders()
MLRNCustomHeaders().addHeader("sECrEt", forHeaderName: "Authorization")
were not recognized in my Swift AppDelegate. I wasn’t sure how to replace or migrate this logic in v11, so I stayed on v10.2.0.
Ask: if v11 introduces a new/official way to attach headers to all MapLibre requests (style, glyphs, sprites, tiles) on iOS release builds, could you please point me to the migration path or sample code? A short snippet or docs link would help me re-test on v11.
This compilation/migration issue is separate from the current problem: the missing Authorization header in iOS TestFlight builds happens on v10.2.0 with code that otherwise works fine locally.
Reproduction steps
- Launch iOS app locally with
npx expo run:ios(Debug/Release local).
✅ Tiles load and backend logs showauthorization: Bearer …for all/{z}/{x}/{y}.pbf. - Build with EAS for iOS and distribute via TestFlight (no code changes).
- Open the same screen, same user/session.
❌ Tiles fail; backend logs show noauthorizationheader on the tile requests (other in-app Axios prewarm requests do include it).
What I observe
- Dev (local):
Authorizationheader is present on all tile requests. - TestFlight:
Authorizationheader missing only on tile requests. - Axios-based prewarm I do (outside MapLibre) reaches the same endpoints with the header in both dev and TF.
- Changing header case to
"authorization"vs"Authorization"does not help. - Re-applying on
AppStateforeground does not help. - Header is added before the map renders and also after (re-applied).
- The domain is HTTPS with a public CA; ATS is satisfied; requests reach the server.
What I tried
addCustomHeader("Authorization", "Bearer …")and also lowercase key.- Remove + re-add around app foreground/background.
- Delay map mount until after header application (see guards above).
- Ensure single
MapViewinstance, no remounts. - Confirmed no proxy/CDN stripping headers.
- Confirmed the same token used by Axios prewarm succeeds (200) while MapLibre tile fetch (same URL) is unauthorized (no header server-side).
What would help to diagnose
- Confirmation that
addCustomHeadershould affect all HTTP requests MapLibre performs (style, sources, sprites, glyphs, tiles) in iOS release builds. - Any known differences between Debug vs Release that affect custom headers for tile requests.
- Guidance on how to inspect/dump the headers MapLibre is attaching on iOS (e.g., a debug hook or logging flag on the native side).
- If this is a bug, a pointer to where in iOS code headers are attached so I can open a PR.
Workarounds
- I can “prewarm” the Koop cache using Axios with auth to reduce load, but this doesn’t fix the missing header on MapLibre tile fetches themselves.
Additional notes
- The style sources are sometimes rewritten at runtime to append query params (e.g.,
?lsPaisIds=[123]). This does not change whether the header appears server-side; only TestFlight vs local makes the difference. - The same behavior reproduces on Wi-Fi and cellular.
TL;DR
addCustomHeader('Authorization', 'Bearer …') works locally but the header disappears in iOS TestFlight for vector tile requests only. Looks like headers set via the global API aren’t applied to tile fetches in release.
@maplibre/maplibre-react-native Version
10.2.0
Which platforms does this occur on?
iOS Device
Which frameworks does this occur on?
Expo
Which architectures does this occur on?
New Architecture
Environment
expo-env-info 2.0.7 environment info:
System:
OS: macOS 15.4.1
Shell: 5.9 - /bin/zsh
Binaries:
Node: 22.18.0 - ~/.nvm/versions/node/v22.18.0/bin/node
Yarn: 1.22.22 - /usr/local/bin/yarn
npm: 10.9.3 - ~/.nvm/versions/node/v22.18.0/bin/npm
Watchman: 2025.03.10.00 - /usr/local/bin/watchman
Managers:
CocoaPods: 1.16.2 - /usr/local/bin/pod
SDKs:
iOS SDK:
Platforms: DriverKit 24.5, iOS 18.5, macOS 15.5, tvOS 18.5, visionOS 2.5, watchOS 11.5
IDEs:
Android Studio: 2024.3 AI-243.24978.46.2431.13208083
Xcode: 16.4/16F6 - /usr/bin/xcodebuild
npmPackages:
expo: 54.0.10 => 54.0.10
expo-router: ~6.0.8 => 6.0.8
expo-updates: ~29.0.11 => 29.0.11
react: 19.1.0 => 19.1.0
react-dom: 19.1.0 => 19.1.0
react-native: 0.81.4 => 0.81.4
react-native-web: ^0.21.0 => 0.21.1
npmGlobalPackages:
eas-cli: 16.19.3
expo-cli: 6.3.12
Expo Workflow: bare