Skip to content

Commit 2849f76

Browse files
committed
feat: add HTTP interceptor support
# Conflicts: # CHANGELOG.md # ios/mapbox_maps_flutter/Sources/mapbox_maps_flutter/Classes/MapboxMapController.swift # lib/src/mapbox_map.dart
1 parent e92090c commit 2849f76

14 files changed

Lines changed: 1734 additions & 100 deletions

CHANGELOG.md

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,37 @@
44
* Introduce new `LineLayer.lineElevationGroundScale` property to scale elevated lines with terrain exaggeration.
55
* Promote elevated lines properties to stable: `LineLayer.lineZOffset` and `LineLayer.lineElevationReference`.
66
* Add experimental `ModelLayer.modelAllowDensityReduction` property to disable density reduction in model layers.
7+
* Add HTTP request/response interceptor API for dynamic header injection and request monitoring.
8+
* `MapboxMapsOptions.setHttpRequestInterceptor()` - Intercept and modify HTTP requests before they are sent. Supports modifying URL, headers, and body.
9+
* `MapboxMapsOptions.setHttpResponseInterceptor()` - Inspect HTTP responses after they are received.
10+
* `MapboxMapsOptions.setCustomHeaders()` - Set static headers that are applied to all requests.
11+
* `HttpInterceptorRequest` - Represents an intercepted request with `url`, `method`, `headers`, and `body` properties. Use `copyWith()` to create modified requests.
12+
* `HttpInterceptorResponse` - Represents an intercepted response with `url`, `statusCode`, `headers`, `data`, and `requestHeaders` properties. The `requestHeaders` field contains the original request headers, useful for correlating requests with responses using custom headers like `X-Request-Id`.
13+
14+
**Important**: These are static methods on `MapboxMapsOptions`, not instance methods on `MapboxMap`. This ensures ALL HTTP requests are intercepted, including the initial style and tile requests made during map initialization. Set up interceptors before creating any `MapWidget`.
15+
16+
Example usage:
17+
```dart
18+
// In initState() or before creating MapWidget:
19+
20+
// Set static headers (applied to all requests)
21+
MapboxMapsOptions.setCustomHeaders({'X-App-Version': '1.0.0'});
22+
23+
// Add dynamic custom headers to requests
24+
MapboxMapsOptions.setHttpRequestInterceptor((request) async {
25+
if (request.url.contains('api.mapbox.com')) {
26+
return request.copyWith(
27+
headers: {...request.headers, 'Authorization': 'Bearer token'},
28+
);
29+
}
30+
return null; // Use original request
31+
});
32+
33+
// Monitor responses
34+
MapboxMapsOptions.setHttpResponseInterceptor((response) async {
35+
print('Response: ${response.statusCode} for ${response.url}');
36+
});
37+
```
738

839
### 2.19.0-rc.1
940

android/src/main/kotlin/com/mapbox/maps/mapbox_maps/MapboxMapController.kt

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -360,6 +360,19 @@ class MapboxMapController(
360360
result.error("HEADER_ERROR", e.message, null)
361361
}
362362
}
363+
"map#setHttpInterceptorEnabled" -> {
364+
try {
365+
val enabled = call.argument<Boolean>("enabled") ?: false
366+
val interceptRequests = call.argument<Boolean>("interceptRequests") ?: false
367+
val interceptResponses = call.argument<Boolean>("interceptResponses") ?: false
368+
val interceptor = CustomHttpServiceInterceptor.getInstance()
369+
interceptor.setFlutterChannel(methodChannel)
370+
interceptor.setInterceptorEnabled(enabled, interceptRequests, interceptResponses)
371+
result.success(null)
372+
} catch (e: Exception) {
373+
result.error("INTERCEPTOR_ERROR", e.message, null)
374+
}
375+
}
363376
else -> {
364377
result.notImplemented()
365378
}

android/src/main/kotlin/com/mapbox/maps/mapbox_maps/MapboxMapsPlugin.kt

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package com.mapbox.maps.mapbox_maps
22

33
import android.content.Context
44
import androidx.lifecycle.Lifecycle
5+
import com.mapbox.maps.mapbox_maps.http.CustomHttpServiceInterceptor
56
import com.mapbox.maps.mapbox_maps.offline.OfflineMapInstanceManager
67
import com.mapbox.maps.mapbox_maps.offline.OfflineSwitch
78
import com.mapbox.maps.mapbox_maps.pigeons._MapboxMapsOptions
@@ -17,10 +18,12 @@ import io.flutter.embedding.engine.plugins.activity.ActivityAware
1718
import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding
1819
import io.flutter.embedding.engine.plugins.lifecycle.FlutterLifecycleAdapter
1920
import io.flutter.plugin.common.BinaryMessenger
21+
import io.flutter.plugin.common.MethodChannel
2022

2123
/** MapboxMapsPlugin */
2224
class MapboxMapsPlugin : FlutterPlugin, ActivityAware {
2325
private var lifecycle: Lifecycle? = null
26+
private var httpInterceptorChannel: MethodChannel? = null
2427

2528
override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
2629
flutterPluginBinding
@@ -54,6 +57,47 @@ class MapboxMapsPlugin : FlutterPlugin, ActivityAware {
5457
_TileStoreInstanceManager.setUp(binaryMessenger, offlineMapInstanceManager)
5558
_OfflineSwitch.setUp(binaryMessenger, offlineSwitch)
5659
LoggingController.setup(binaryMessenger)
60+
61+
// Setup static HTTP interceptor channel - available before any map is created
62+
setupHttpInterceptorChannel(binaryMessenger)
63+
}
64+
65+
private fun setupHttpInterceptorChannel(binaryMessenger: BinaryMessenger) {
66+
httpInterceptorChannel = MethodChannel(binaryMessenger, "com.mapbox.maps.flutter/http_interceptor")
67+
val interceptor = CustomHttpServiceInterceptor.getInstance()
68+
interceptor.setFlutterChannel(httpInterceptorChannel)
69+
70+
httpInterceptorChannel?.setMethodCallHandler { call, result ->
71+
when (call.method) {
72+
"setCustomHeaders" -> {
73+
try {
74+
val headers = call.argument<Map<String, String>>("headers")
75+
headers?.let {
76+
interceptor.setCustomHeaders(headers)
77+
result.success(null)
78+
} ?: run {
79+
result.error("INVALID_ARGUMENTS", "Headers cannot be null", null)
80+
}
81+
} catch (e: Exception) {
82+
result.error("HEADER_ERROR", e.message, null)
83+
}
84+
}
85+
"setHttpInterceptorEnabled" -> {
86+
try {
87+
val enabled = call.argument<Boolean>("enabled") ?: false
88+
val interceptRequests = call.argument<Boolean>("interceptRequests") ?: false
89+
val interceptResponses = call.argument<Boolean>("interceptResponses") ?: false
90+
interceptor.setInterceptorEnabled(enabled, interceptRequests, interceptResponses)
91+
result.success(null)
92+
} catch (e: Exception) {
93+
result.error("INTERCEPTOR_ERROR", e.message, null)
94+
}
95+
}
96+
else -> {
97+
result.notImplemented()
98+
}
99+
}
100+
}
57101
}
58102

59103
override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) {

android/src/main/kotlin/com/mapbox/maps/mapbox_maps/http/CustomHttpServiceInterceptor.kt

Lines changed: 139 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,154 @@
11
package com.mapbox.maps.mapbox_maps.http
22

3+
import android.os.Handler
4+
import android.os.Looper
35
import com.mapbox.common.HttpRequest
46
import com.mapbox.common.HttpRequestOrResponse
57
import com.mapbox.common.HttpResponse
68
import com.mapbox.common.HttpServiceFactory
79
import com.mapbox.common.HttpServiceInterceptorInterface
810
import com.mapbox.common.HttpServiceInterceptorRequestContinuation
911
import com.mapbox.common.HttpServiceInterceptorResponseContinuation
12+
import io.flutter.plugin.common.MethodChannel
13+
import java.util.concurrent.CountDownLatch
14+
import java.util.concurrent.TimeUnit
1015

1116
class CustomHttpServiceInterceptor : HttpServiceInterceptorInterface {
1217
private var customHeaders: MutableMap<String, String> = mutableMapOf()
18+
private var flutterChannel: MethodChannel? = null
19+
private var interceptRequests: Boolean = false
20+
private var interceptResponses: Boolean = false
21+
private val mainHandler = Handler(Looper.getMainLooper())
1322

1423
override fun onRequest(request: HttpRequest, continuation: HttpServiceInterceptorRequestContinuation) {
1524
val currentHeaders = HashMap(request.headers)
1625

26+
// First apply static custom headers
1727
currentHeaders.putAll(customHeaders)
18-
val modifiedRequest = request.toBuilder()
19-
.headers(currentHeaders)
20-
.build()
28+
29+
// If Flutter callback is enabled, invoke it
30+
if (interceptRequests && flutterChannel != null) {
31+
invokeFlutterRequestCallback(request, currentHeaders, continuation)
32+
} else {
33+
// No callback, just continue with static headers
34+
val modifiedRequest = request.toBuilder()
35+
.headers(currentHeaders)
36+
.build()
37+
val requestOrResponse = HttpRequestOrResponse(modifiedRequest)
38+
continuation.run(requestOrResponse)
39+
}
40+
}
41+
42+
private fun invokeFlutterRequestCallback(
43+
request: HttpRequest,
44+
currentHeaders: HashMap<String, String>,
45+
continuation: HttpServiceInterceptorRequestContinuation
46+
) {
47+
val requestMap = mapOf(
48+
"url" to request.url,
49+
"method" to request.method.name,
50+
"headers" to currentHeaders,
51+
"body" to request.body
52+
)
53+
54+
// Use a latch to wait for the Flutter response
55+
val latch = CountDownLatch(1)
56+
var modifiedRequestMap: Map<String, Any?>? = null
57+
58+
mainHandler.post {
59+
flutterChannel?.invokeMethod("http#onRequest", requestMap, object : MethodChannel.Result {
60+
override fun success(result: Any?) {
61+
@Suppress("UNCHECKED_CAST")
62+
modifiedRequestMap = result as? Map<String, Any?>
63+
latch.countDown()
64+
}
65+
66+
override fun error(errorCode: String, errorMessage: String?, errorDetails: Any?) {
67+
// On error, continue with original request
68+
latch.countDown()
69+
}
70+
71+
override fun notImplemented() {
72+
// Not implemented, continue with original request
73+
latch.countDown()
74+
}
75+
})
76+
}
77+
78+
// Wait for Flutter response with a timeout
79+
val received = latch.await(5, TimeUnit.SECONDS)
80+
81+
val builder = request.toBuilder()
82+
83+
if (received && modifiedRequestMap != null) {
84+
val modified = modifiedRequestMap!!
85+
86+
// Apply modified headers
87+
@Suppress("UNCHECKED_CAST")
88+
val newHeaders = modified["headers"] as? Map<String, String>
89+
if (newHeaders != null) {
90+
builder.headers(HashMap(newHeaders))
91+
} else {
92+
builder.headers(currentHeaders)
93+
}
94+
95+
// Apply modified URL
96+
val newUrl = modified["url"] as? String
97+
if (newUrl != null) {
98+
builder.url(newUrl)
99+
}
100+
101+
// Apply modified body
102+
@Suppress("UNCHECKED_CAST")
103+
val newBody = modified["body"] as? ByteArray
104+
if (newBody != null) {
105+
builder.body(newBody)
106+
}
107+
108+
// Note: method is not modifiable via the builder API
109+
} else {
110+
builder.headers(currentHeaders)
111+
}
112+
113+
val modifiedRequest = builder.build()
21114
val requestOrResponse = HttpRequestOrResponse(modifiedRequest)
22115
continuation.run(requestOrResponse)
23116
}
24117

25118
override fun onResponse(response: HttpResponse, continuation: HttpServiceInterceptorResponseContinuation) {
119+
// If Flutter callback is enabled, invoke it (non-blocking)
120+
if (interceptResponses && flutterChannel != null) {
121+
// response.result is Expected<HttpRequestError, HttpResponseData>
122+
// We need to check if it's a success and extract the value
123+
val responseData = response.result.value
124+
val responseMap = if (responseData != null) {
125+
mapOf(
126+
"url" to response.request.url,
127+
"statusCode" to responseData.code,
128+
"headers" to responseData.headers,
129+
"data" to responseData.data,
130+
"requestHeaders" to response.request.headers
131+
)
132+
} else {
133+
// Error response - still send basic info
134+
mapOf(
135+
"url" to response.request.url,
136+
"statusCode" to -1,
137+
"headers" to emptyMap<String, String>(),
138+
"data" to null,
139+
"requestHeaders" to response.request.headers
140+
)
141+
}
142+
143+
mainHandler.post {
144+
flutterChannel?.invokeMethod("http#onResponse", responseMap, object : MethodChannel.Result {
145+
override fun success(result: Any?) {}
146+
override fun error(errorCode: String, errorMessage: String?, errorDetails: Any?) {}
147+
override fun notImplemented() {}
148+
})
149+
}
150+
}
151+
26152
continuation.run(response)
27153
}
28154

@@ -32,6 +158,16 @@ class CustomHttpServiceInterceptor : HttpServiceInterceptorInterface {
32158
HttpServiceFactory.setHttpServiceInterceptor(this)
33159
}
34160

161+
fun setFlutterChannel(channel: MethodChannel?) {
162+
flutterChannel = channel
163+
}
164+
165+
fun setInterceptorEnabled(enabled: Boolean, interceptRequests: Boolean, interceptResponses: Boolean) {
166+
this.interceptRequests = enabled && interceptRequests
167+
this.interceptResponses = enabled && interceptResponses
168+
HttpServiceFactory.setHttpServiceInterceptor(this)
169+
}
170+
35171
companion object {
36172
private var instance: CustomHttpServiceInterceptor? = null
37173

0 commit comments

Comments
 (0)