feat: add HTTP interceptor support#1101
Conversation
|
@mapbox/mapbox-maps-flutter - @evil159 - Any thoughts here? |
|
Hi @dballance -- Thanks for the PR! We'll take a look early next week. 😀 |
|
Great - thanks - happy to rework or extend if needed! |
# Conflicts: # CHANGELOG.md # ios/mapbox_maps_flutter/Sources/mapbox_maps_flutter/Classes/MapboxMapController.swift # lib/src/mapbox_map.dart
4015162 to
2849f76
Compare
|
Hi @dballance -- A few things came up earlier this week, but this is first on my to do list tomorrow. |
|
Thanks again for the contribution @dballance! The Dart-side API design looks good: MapboxMapsOptions as the entry point,
This is the biggest issue. Both platforms use the same pattern: block the calling thread with a semaphore/latch, post to the main thread to invoke Flutter, and wait for the response. The problem is that
The PR adds
Sending the full response body to Flutter on every intercepted response is a concern. Tile bodies are regularly 200–500 KB and during initial map load you can have dozens of these completing at once. Serializing them through the platform channel just for logging purposes may cause memory spikes and slow down map initialization noticeably. The response interceptor should omit data by default, with an explicit opt-in if body inspection is genuinely needed.
CustomHttpServiceInterceptor is a singleton whose mutable fields (customHeaders, flutterChannel, interceptRequests, interceptResponses) are written from the Flutter main thread and read from the HTTP network thread with no synchronization. I think a lock is needed here.
The deadlock won't show up in a simple unit test because you need the main thread to be the caller, but an integration test that creates a MapWidget with an interceptor already set should catch it. The response body performance issue also needs a test or at least a benchmark, since it's the kind of thing that looks fine in isolation but could degrade at scale. |
|
Thanks @pjleonard37! I mostly got this done, I think. Just ran all the integration tests locally and the sample app runs as well. I'm curious about testing for response body - what kind of benchmark were you thinking? I added some tests but unsure it covers what you're thinking about. |
Thanks for the edits and the test. I added a few more comments/updates, but agree we're close to completion. I thought more about testing and I think we're good without any additional benchmarking. Now that body is off by default I think we're good 👍 |
- iOS: fix compile error by using property assignment instead of non-existent setCustomHeaders() method; also register the interceptor via HttpServiceFactory so custom headers actually take effect - Android: tear down httpInterceptorChannel and clear the Flutter channel reference in onDetachedFromEngine to prevent stale handlers on hot restart - Dart: replace print() with developer.log() in http#onResponse error handler to use existing logging infrastructure - Dart: remove 'method' parameter from HttpInterceptorRequest.copyWith() since the HTTP method cannot be changed by an interceptor - Dart: add @deprecated annotation to MapboxHttpService.setCustomHeaders() and delegate to MapboxMapsOptions.setCustomHeaders() - CHANGELOG: add migration note for users of the deprecated instance method - Integration test: add setUp() that calls MapboxMapsOptions.clearData() to ensure a clean cache state before each test
836a720 to
d45d75a
Compare
|
@pjleonard37 - I updated this shortly after the last comment - is something else missing here? |
|
Hoping we can get this into a 3.0.0 release! |
There was a problem hiding this comment.
Pull request overview
Adds a Dart-level API for global HTTP request/response interception (and static custom headers) that can be configured before any MapWidget is created, matching native SDK behavior and addressing the “first request goes out without headers” problem (e.g., #1062).
Changes:
- Introduces
MapboxMapsOptionsstatic APIs to set global custom headers and register HTTP request/response interceptors. - Implements a new platform channel (
com.mapbox.maps.flutter/http_interceptor) and native interceptor wiring on iOS/Android so interception works during map initialization. - Updates examples, changelog, and adds integration tests covering deadlock prevention and basic interceptor behavior.
Reviewed changes
Copilot reviewed 16 out of 16 changed files in this pull request and generated 10 comments.
Show a summary per file
| File | Description |
|---|---|
| lib/src/mapbox_maps_platform.dart | Ensures the default method-call handler returns a value (null). |
| lib/src/mapbox_maps_options.dart | Adds the new public static interceptor/header APIs (docs included). |
| lib/src/mapbox_map.dart | Changes setCustomHeaders to delegate to the new global options API. |
| lib/src/http/http_service.dart | Adds HttpInterceptorRequest/Response models + deprecates instance setCustomHeaders. |
| lib/src/http/http_interceptor_controller.dart | New Dart-side controller managing interceptor callbacks + method-channel state. |
| lib/mapbox_maps_flutter.dart | Registers the new controller as a part of the library. |
| ios/.../MapboxMapsPlugin.swift | Creates the static interceptor method channel before any map is created. |
| ios/.../MapboxMapController.swift | Removes per-map map#setCustomHeaders handling (now global). |
| ios/.../CustomHttpServiceInterceptor.swift | Adds thread-safe singleton interceptor with request/response callbacks into Flutter. |
| android/.../MapboxMapsPlugin.kt | Creates the static interceptor method channel before any map is created; cleans up on detach. |
| android/.../MapboxMapController.kt | Removes per-map map#setCustomHeaders handling (now global). |
| android/.../http/CustomHttpServiceInterceptor.kt | Adds thread-safe singleton interceptor with request/response callbacks into Flutter. |
| example/lib/custom_header_example.dart | Demonstrates static headers + request/response interception and logging UI. |
| example/integration_test/http_interceptor_test.dart | New integration tests for deadlock prevention, headers, body opt-in, and cleanup. |
| example/integration_test/all_test.dart | Adds the new interceptor test suite to the test runner. |
| CHANGELOG.md | Documents new APIs, migration guidance, and usage examples. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| late final MapboxHttpService httpService = MapboxHttpService( | ||
| binaryMessenger: _mapboxMapsPlatform.binaryMessenger, | ||
| channelSuffix: _mapboxMapsPlatform.channelSuffix); | ||
|
|
| Future<void> setCustomHeaders(Map<String, String> headers) async { | ||
| await _channel.invokeMethod('setCustomHeaders', {'headers': headers}); | ||
| } |
| Future<void> _updateInterceptorState() async { | ||
| final shouldEnable = | ||
| _requestInterceptor != null || _responseInterceptor != null; | ||
| final interceptRequests = _requestInterceptor != null; | ||
| final interceptResponses = _responseInterceptor != null; | ||
|
|
||
| await _channel.invokeMethod('setHttpInterceptorEnabled', { | ||
| 'enabled': shouldEnable, | ||
| 'interceptRequests': interceptRequests, | ||
| 'interceptResponses': interceptResponses, | ||
| 'includeResponseBody': _includeResponseBody, | ||
| }); | ||
| } |
| /// If [includeResponseBody] is true, the response body will be included in | ||
| /// the [HttpInterceptorResponse.body] field. This defaults to false to avoid | ||
| /// performance issues with large tile bodies (200-500KB per tile). Only | ||
| /// enable this if you need to inspect the actual response body content. |
| // Set up request interceptor for dynamic header injection | ||
| MapboxMapsOptions.setHttpRequestInterceptor((request) async { | ||
| // Generate a unique ID for this request at the Dart level. |
| // Optionally set up response interceptor for logging/monitoring | ||
| MapboxMapsOptions.setHttpResponseInterceptor((response) async { | ||
| // The request ID is available via the roundtripped request headers. |
| } else { | ||
| // Disable interceptors | ||
| MapboxMapsOptions.setHttpRequestInterceptor(null); | ||
| MapboxMapsOptions.setHttpResponseInterceptor(null); | ||
| } |
| @override | ||
| void initState() { | ||
| super.initState(); | ||
| // Set up interceptors BEFORE the map is created. | ||
| // This ensures ALL requests are intercepted, including | ||
| // the initial style and tile requests during map initialization. | ||
| MapboxMapsOptions.setCustomHeaders({'X-Static-Header': 'static-value'}); | ||
| _setupHttpInterceptor(); | ||
| } |
| void dispose() { | ||
| // Clean up using static methods | ||
| MapboxMapsOptions.setCustomHeaders({}); | ||
| MapboxMapsOptions.setHttpRequestInterceptor(null); | ||
| MapboxMapsOptions.setHttpResponseInterceptor(null); |
| tearDown(() async { | ||
| // Clean up interceptors after each test | ||
| MapboxMapsOptions.setHttpRequestInterceptor(null); | ||
| MapboxMapsOptions.setHttpResponseInterceptor(null); | ||
| MapboxMapsOptions.setCustomHeaders({}); | ||
| }); |
What does this pull request do?
Adds HTTP interceptor support, in Dart, to match up to the native SDK functionality.
The interceptors are added before a map is instantiated, so that all requests (including fonts, images, etc) can be modified / logged / observed when the map is created. All map instances will share the same interceptor, much like the existing SDK functionality.
I'm opening this first to get feedback on the overall approach - will add tests if needed. Seems most of the native code is only tested indirectly, so would potentially just be an integration test of some sort.
What is the motivation and context behind this change?
Allow per request header modifications, logging, and tracing spans on map requests. Would potentially close #1062 and other stories.
Pull request checklist:
PRs must be submitted under the terms of our Contributor License Agreement CLA.