Skip to main content

maplibre_native/
resource.rs

1use std::cell::Cell;
2use std::fmt;
3use std::marker::PhantomData;
4use std::os::raw::{c_char, c_void};
5use std::panic::{AssertUnwindSafe, catch_unwind};
6use std::ptr;
7use std::sync::Arc;
8
9use maplibre_native_core as maplibre_core;
10use maplibre_native_sys as sys;
11
12use crate::{HandleOperationError, Result};
13
14pub use maplibre_core::resource::{
15    ByteRange, ResourceProviderDecision, ResourceRequest, ResourceResponse,
16    ResourceTransformRequest,
17};
18
19use maplibre_core::resource::{
20    ResourceRequestHandleFns, ResourceRequestHandleState, UNKNOWN_PROVIDER_DECISION,
21};
22
23/// Owned handle for a resource provider request selected for handling.
24///
25/// The handle may be sent to another thread for deferred completion. It is
26/// one-shot: call `complete` once to provide a response, or `close`/drop it to
27/// release the provider's reference without completing.
28pub struct ResourceRequestHandle {
29    state: Arc<ResourceRequestHandleState>,
30    _not_sync: PhantomData<Cell<()>>,
31}
32
33impl fmt::Debug for ResourceRequestHandle {
34    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
35        f.debug_struct("ResourceRequestHandle")
36            .finish_non_exhaustive()
37    }
38}
39
40impl ResourceRequestHandle {
41    fn from_state(state: Arc<ResourceRequestHandleState>) -> Self {
42        Self {
43            state,
44            _not_sync: PhantomData,
45        }
46    }
47
48    fn from_raw_with_fns(
49        handle: *mut sys::mln_resource_request_handle,
50        fns: ResourceRequestHandleFns,
51    ) -> Result<Self> {
52        // SAFETY: handle is received from the resource-provider C callback and
53        // fns matches that native handle type.
54        unsafe { ResourceRequestHandleState::new(handle, fns) }.map(Self::from_state)
55    }
56
57    /// Completes the request. Successful completion releases this handle once
58    /// the provider callback has returned `Handle` to native code.
59    ///
60    /// When a provider callback completes inline and then returns
61    /// [`ResourceProviderDecision::PassThrough`], the wrapper still returns the
62    /// native `Handle` decision. Native code must not also pass the completed
63    /// request through to its own networking path.
64    pub fn complete(
65        self,
66        response: ResourceResponse,
67    ) -> std::result::Result<(), HandleOperationError<Self>> {
68        self.state
69            .complete(&response)
70            .map_err(|error| HandleOperationError::new(error, self))
71    }
72
73    /// Reports whether native code has cancelled the request.
74    pub fn is_cancelled(&self) -> Result<bool> {
75        self.state.is_cancelled()
76    }
77
78    /// Releases the provider-owned request handle without completing it.
79    pub fn close(self) {
80        self.state.close();
81    }
82}
83
84impl Drop for ResourceRequestHandle {
85    fn drop(&mut self) {
86        self.state.close();
87    }
88}
89
90type ResourceProviderCallback = dyn Fn(ResourceRequest, ResourceRequestHandle) -> ResourceProviderDecision
91    + Send
92    + Sync
93    + 'static;
94
95pub(crate) struct ResourceProviderState {
96    callback: Box<ResourceProviderCallback>,
97    handle_fns: ResourceRequestHandleFns,
98}
99
100impl fmt::Debug for ResourceProviderState {
101    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
102        f.debug_struct("ResourceProviderState")
103            .finish_non_exhaustive()
104    }
105}
106
107impl ResourceProviderState {
108    pub(crate) fn new<F>(callback: F) -> Box<Self>
109    where
110        F: Fn(ResourceRequest, ResourceRequestHandle) -> ResourceProviderDecision
111            + Send
112            + Sync
113            + 'static,
114    {
115        Box::new(Self {
116            callback: Box::new(callback),
117            handle_fns: ResourceRequestHandleFns::NATIVE,
118        })
119    }
120
121    pub(crate) fn descriptor(&self) -> sys::mln_resource_provider {
122        maplibre_core::resource::resource_provider_descriptor(
123            Some(resource_provider_trampoline),
124            ptr::from_ref(self).cast_mut().cast::<c_void>(),
125        )
126    }
127
128    fn invoke(
129        &self,
130        request: *const sys::mln_resource_request,
131        handle: *mut sys::mln_resource_request_handle,
132    ) -> u32 {
133        let Some(raw_request) = ptr::NonNull::new(request.cast_mut()) else {
134            return UNKNOWN_PROVIDER_DECISION;
135        };
136        let handle = match ResourceRequestHandle::from_raw_with_fns(handle, self.handle_fns) {
137            Ok(handle) => handle,
138            Err(_) => return UNKNOWN_PROVIDER_DECISION,
139        };
140        let state = Arc::clone(&handle.state);
141
142        // SAFETY: raw_request is non-null and borrowed for the callback duration.
143        let request =
144            match unsafe { maplibre_core::resource::copy_resource_request(raw_request.as_ref()) } {
145                Ok(request) => request,
146                Err(_) => return state.finish_provider_exception(),
147            };
148
149        match catch_unwind(AssertUnwindSafe(|| (self.callback)(request, handle))) {
150            Ok(decision) => state.finish_provider_decision(decision),
151            Err(_) => state.finish_provider_exception(),
152        }
153    }
154}
155
156unsafe extern "C" fn resource_provider_trampoline(
157    user_data: *mut c_void,
158    request: *const sys::mln_resource_request,
159    handle: *mut sys::mln_resource_request_handle,
160) -> u32 {
161    let Some(state) = ptr::NonNull::new(user_data.cast::<ResourceProviderState>()) else {
162        return UNKNOWN_PROVIDER_DECISION;
163    };
164    // SAFETY: user_data is installed from ResourceProviderState::descriptor and
165    // remains valid until replacement or runtime teardown. The callback state is
166    // Send + Sync because native may invoke it from worker/network threads.
167    unsafe { state.as_ref() }.invoke(request, handle)
168}
169
170type ResourceTransformCallback =
171    dyn Fn(ResourceTransformRequest) -> Option<String> + Send + Sync + 'static;
172
173pub(crate) struct ResourceTransformState {
174    callback: Box<ResourceTransformCallback>,
175}
176
177impl fmt::Debug for ResourceTransformState {
178    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
179        f.debug_struct("ResourceTransformState")
180            .finish_non_exhaustive()
181    }
182}
183
184impl ResourceTransformState {
185    pub(crate) fn new<F>(callback: F) -> Box<Self>
186    where
187        F: Fn(ResourceTransformRequest) -> Option<String> + Send + Sync + 'static,
188    {
189        Box::new(Self {
190            callback: Box::new(callback),
191        })
192    }
193
194    pub(crate) fn descriptor(&self) -> sys::mln_resource_transform {
195        maplibre_core::resource::resource_transform_descriptor(
196            Some(resource_transform_trampoline),
197            ptr::from_ref(self).cast_mut().cast::<c_void>(),
198        )
199    }
200
201    fn invoke(
202        &self,
203        raw_kind: u32,
204        url: *const c_char,
205        out_response: *mut sys::mln_resource_transform_response,
206    ) -> sys::mln_status {
207        // SAFETY: out_response is callback-duration output storage provided by
208        // native; core validates null before initializing it.
209        let status = unsafe {
210            maplibre_core::resource::initialize_resource_transform_response(out_response)
211        };
212        if status != sys::MLN_STATUS_OK {
213            return status;
214        }
215
216        // SAFETY: url is borrowed for the callback duration by the C API.
217        let request = match unsafe {
218            maplibre_core::resource::copy_resource_transform_request(raw_kind, url)
219        } {
220            Ok(request) => request,
221            Err(error) => return maplibre_core::resource::status_for_error(&error),
222        };
223
224        let replacement = match catch_unwind(AssertUnwindSafe(|| (self.callback)(request))) {
225            Ok(replacement) => replacement,
226            Err(_) => return sys::MLN_STATUS_NATIVE_ERROR,
227        };
228
229        match replacement {
230            Some(replacement) if !replacement.is_empty() => {
231                // SAFETY: out_response was checked by
232                // initialize_resource_transform_response. The helper copies the
233                // temporary Rust string into C API-managed callback scratch
234                // storage that remains live while native copies it after this
235                // trampoline returns.
236                unsafe {
237                    sys::mln_resource_transform_response_set_url(
238                        out_response,
239                        replacement.as_ptr().cast(),
240                        replacement.len(),
241                    )
242                }
243            }
244            _ => sys::MLN_STATUS_OK,
245        }
246    }
247}
248
249unsafe extern "C" fn resource_transform_trampoline(
250    user_data: *mut c_void,
251    kind: u32,
252    url: *const c_char,
253    out_response: *mut sys::mln_resource_transform_response,
254) -> sys::mln_status {
255    let Some(state) = ptr::NonNull::new(user_data.cast::<ResourceTransformState>()) else {
256        return sys::MLN_STATUS_INVALID_ARGUMENT;
257    };
258    // SAFETY: user_data is installed from ResourceTransformState::descriptor
259    // and remains valid until the runtime replaces/clears the transform or is
260    // destroyed. The callback state itself is Send + Sync.
261    unsafe { state.as_ref() }.invoke(kind, url, out_response)
262}
263
264#[cfg(test)]
265mod tests {
266    use std::ffi::CString;
267    use std::sync::Mutex as StdMutex;
268    use std::sync::atomic::{AtomicBool, AtomicI32, AtomicUsize, Ordering};
269
270    use static_assertions::{assert_impl_all, assert_not_impl_any};
271
272    use super::*;
273    use crate::{
274        ErrorKind, ResourceKind, ResourceLoadingMethod, ResourcePriority, ResourceStoragePolicy,
275        ResourceUsage,
276    };
277
278    static FAKE_HANDLE_TEST_LOCK: StdMutex<()> = StdMutex::new(());
279    static COMPLETE_COUNT: AtomicUsize = AtomicUsize::new(0);
280    static RELEASE_COUNT: AtomicUsize = AtomicUsize::new(0);
281    static CANCELLED_VALUE: AtomicBool = AtomicBool::new(false);
282    static COMPLETE_STATUS: AtomicI32 = AtomicI32::new(sys::MLN_STATUS_OK);
283
284    unsafe extern "C" fn fake_complete(
285        _handle: *mut sys::mln_resource_request_handle,
286        _response: *const sys::mln_resource_response,
287    ) -> sys::mln_status {
288        COMPLETE_COUNT.fetch_add(1, Ordering::SeqCst);
289        COMPLETE_STATUS.load(Ordering::SeqCst)
290    }
291
292    unsafe extern "C" fn fake_cancelled(
293        _handle: *const sys::mln_resource_request_handle,
294        out_cancelled: *mut bool,
295    ) -> sys::mln_status {
296        if out_cancelled.is_null() {
297            return sys::MLN_STATUS_INVALID_ARGUMENT;
298        }
299        // SAFETY: out_cancelled is non-null and points to caller-owned output storage.
300        unsafe { *out_cancelled = CANCELLED_VALUE.load(Ordering::SeqCst) };
301        sys::MLN_STATUS_OK
302    }
303
304    unsafe extern "C" fn fake_release(_handle: *mut sys::mln_resource_request_handle) {
305        RELEASE_COUNT.fetch_add(1, Ordering::SeqCst);
306    }
307
308    fn fake_fns() -> ResourceRequestHandleFns {
309        // SAFETY: These fake functions implement the same ownership contract as
310        // the native handle functions for tests.
311        unsafe { ResourceRequestHandleFns::new(fake_complete, fake_cancelled, fake_release) }
312    }
313
314    fn reset_fake_handle_state() {
315        COMPLETE_COUNT.store(0, Ordering::SeqCst);
316        RELEASE_COUNT.store(0, Ordering::SeqCst);
317        CANCELLED_VALUE.store(false, Ordering::SeqCst);
318        COMPLETE_STATUS.store(sys::MLN_STATUS_OK, Ordering::SeqCst);
319    }
320
321    fn fake_handle() -> ResourceRequestHandle {
322        reset_fake_handle_state();
323        ResourceRequestHandle::from_raw_with_fns(
324            0x1234usize as *mut sys::mln_resource_request_handle,
325            fake_fns(),
326        )
327        .unwrap()
328    }
329
330    fn request() -> sys::mln_resource_request {
331        sys::mln_resource_request {
332            size: std::mem::size_of::<sys::mln_resource_request>() as u32,
333            url: c"https://example.test/tile.pbf".as_ptr(),
334            kind: sys::MLN_RESOURCE_KIND_TILE,
335            loading_method: sys::MLN_RESOURCE_LOADING_METHOD_NETWORK_ONLY,
336            priority: sys::MLN_RESOURCE_PRIORITY_LOW,
337            usage: sys::MLN_RESOURCE_USAGE_OFFLINE,
338            storage_policy: sys::MLN_RESOURCE_STORAGE_POLICY_VOLATILE,
339            has_range: true,
340            range_start: 7,
341            range_end: 11,
342            has_prior_modified: true,
343            prior_modified_unix_ms: 123,
344            has_prior_expires: true,
345            prior_expires_unix_ms: 456,
346            prior_etag: c"etag".as_ptr(),
347            prior_data: [1u8, 2, 3].as_ptr(),
348            prior_data_size: 3,
349        }
350    }
351
352    fn response() -> sys::mln_resource_transform_response {
353        sys::mln_resource_transform_response {
354            size: std::mem::size_of::<sys::mln_resource_transform_response>() as u32,
355            url: ptr::null(),
356            context: ptr::null_mut(),
357        }
358    }
359
360    #[test]
361    fn resource_request_handle_is_send_but_not_sync() {
362        assert_impl_all!(ResourceRequestHandle: Send);
363        assert_not_impl_any!(ResourceRequestHandle: Sync);
364    }
365
366    #[test]
367    fn provider_callback_copies_request_and_pass_through_does_not_release() {
368        let _guard = FAKE_HANDLE_TEST_LOCK.lock().unwrap();
369        reset_fake_handle_state();
370        let state = ResourceProviderState {
371            callback: Box::new(|request, handle| {
372                assert_eq!(request.url, "https://example.test/tile.pbf");
373                assert_eq!(request.kind, ResourceKind::Tile);
374                assert_eq!(request.raw_kind, sys::MLN_RESOURCE_KIND_TILE);
375                assert_eq!(request.loading_method, ResourceLoadingMethod::NetworkOnly);
376                assert_eq!(request.priority, ResourcePriority::Low);
377                assert_eq!(request.usage, ResourceUsage::Offline);
378                assert_eq!(request.storage_policy, ResourceStoragePolicy::Volatile);
379                let range = request.range.unwrap();
380                assert_eq!((range.start, range.end), (7, 11));
381                assert_eq!(request.prior_modified_unix_ms, Some(123));
382                assert_eq!(request.prior_expires_unix_ms, Some(456));
383                assert_eq!(request.prior_etag.as_deref(), Some("etag"));
384                assert_eq!(request.prior_data, vec![1, 2, 3]);
385                drop(handle);
386                ResourceProviderDecision::PassThrough
387            }),
388            handle_fns: fake_fns(),
389        };
390        let raw_request = request();
391
392        let decision = state.invoke(
393            &raw_request,
394            0x1234usize as *mut sys::mln_resource_request_handle,
395        );
396
397        assert_eq!(decision, sys::MLN_RESOURCE_PROVIDER_DECISION_PASS_THROUGH);
398        assert_eq!(RELEASE_COUNT.load(Ordering::SeqCst), 0);
399    }
400
401    #[test]
402    fn inline_completion_returns_handle_and_releases_once() {
403        let _guard = FAKE_HANDLE_TEST_LOCK.lock().unwrap();
404        reset_fake_handle_state();
405        let state = ResourceProviderState {
406            callback: Box::new(|_, handle| {
407                handle.complete(ResourceResponse::ok([1, 2, 3])).unwrap();
408                ResourceProviderDecision::PassThrough
409            }),
410            handle_fns: fake_fns(),
411        };
412        let raw_request = request();
413
414        let decision = state.invoke(
415            &raw_request,
416            0x1234usize as *mut sys::mln_resource_request_handle,
417        );
418
419        assert_eq!(decision, sys::MLN_RESOURCE_PROVIDER_DECISION_HANDLE);
420        assert_eq!(COMPLETE_COUNT.load(Ordering::SeqCst), 1);
421        assert_eq!(RELEASE_COUNT.load(Ordering::SeqCst), 1);
422    }
423
424    #[test]
425    fn deferred_completion_from_another_thread_releases_once() {
426        let _guard = FAKE_HANDLE_TEST_LOCK.lock().unwrap();
427        let handle = fake_handle();
428        assert_eq!(
429            handle
430                .state
431                .finish_provider_decision(ResourceProviderDecision::Handle),
432            sys::MLN_RESOURCE_PROVIDER_DECISION_HANDLE
433        );
434
435        let thread = std::thread::spawn(move || {
436            handle
437                .complete(ResourceResponse::ok(vec![4, 5, 6]))
438                .unwrap();
439        });
440        thread.join().unwrap();
441
442        assert_eq!(COMPLETE_COUNT.load(Ordering::SeqCst), 1);
443        assert_eq!(RELEASE_COUNT.load(Ordering::SeqCst), 1);
444    }
445
446    #[test]
447    fn failed_completion_returns_handle_for_retry() {
448        let _guard = FAKE_HANDLE_TEST_LOCK.lock().unwrap();
449        let handle = fake_handle();
450        COMPLETE_STATUS.store(sys::MLN_STATUS_INVALID_STATE, Ordering::SeqCst);
451
452        let error = handle
453            .complete(ResourceResponse::ok(Vec::new()))
454            .unwrap_err();
455        assert_eq!(error.kind(), ErrorKind::InvalidState);
456        assert_eq!(COMPLETE_COUNT.load(Ordering::SeqCst), 1);
457
458        COMPLETE_STATUS.store(sys::MLN_STATUS_OK, Ordering::SeqCst);
459        let handle = error.into_handle();
460        handle.complete(ResourceResponse::no_content()).unwrap();
461        assert_eq!(COMPLETE_COUNT.load(Ordering::SeqCst), 2);
462    }
463
464    #[test]
465    fn drop_releases_uncompleted_handled_request_once() {
466        let _guard = FAKE_HANDLE_TEST_LOCK.lock().unwrap();
467        let handle = fake_handle();
468        assert_eq!(
469            handle
470                .state
471                .finish_provider_decision(ResourceProviderDecision::Handle),
472            sys::MLN_RESOURCE_PROVIDER_DECISION_HANDLE
473        );
474        drop(handle);
475
476        assert_eq!(RELEASE_COUNT.load(Ordering::SeqCst), 1);
477    }
478
479    #[test]
480    fn close_before_handle_decision_releases_after_decision() {
481        let _guard = FAKE_HANDLE_TEST_LOCK.lock().unwrap();
482        let handle = fake_handle();
483        let state = Arc::clone(&handle.state);
484        handle.close();
485        assert_eq!(RELEASE_COUNT.load(Ordering::SeqCst), 0);
486        assert_eq!(
487            state.finish_provider_decision(ResourceProviderDecision::Handle),
488            sys::MLN_RESOURCE_PROVIDER_DECISION_HANDLE
489        );
490
491        assert_eq!(RELEASE_COUNT.load(Ordering::SeqCst), 1);
492    }
493
494    #[test]
495    fn cancelled_queries_use_native_function_until_closed() {
496        let _guard = FAKE_HANDLE_TEST_LOCK.lock().unwrap();
497        let handle = fake_handle();
498        CANCELLED_VALUE.store(true, Ordering::SeqCst);
499
500        assert!(handle.is_cancelled().unwrap());
501        handle.close();
502    }
503
504    #[test]
505    fn provider_panics_produce_unknown_decision_without_unwinding() {
506        let _guard = FAKE_HANDLE_TEST_LOCK.lock().unwrap();
507        reset_fake_handle_state();
508        let state = ResourceProviderState {
509            callback: Box::new(|_, _| panic!("boom")),
510            handle_fns: fake_fns(),
511        };
512        let raw_request = request();
513
514        let decision = state.invoke(
515            &raw_request,
516            0x1234usize as *mut sys::mln_resource_request_handle,
517        );
518
519        assert_eq!(decision, UNKNOWN_PROVIDER_DECISION);
520        assert_eq!(RELEASE_COUNT.load(Ordering::SeqCst), 0);
521    }
522
523    #[test]
524    fn transform_callback_copies_request_when_keeping_original_url() {
525        let state = ResourceTransformState::new(|request| {
526            assert_eq!(request.kind, ResourceKind::Style);
527            assert_eq!(request.raw_kind, sys::MLN_RESOURCE_KIND_STYLE);
528            assert_eq!(request.url, "https://example.test/style.json");
529            None
530        });
531        let descriptor = state.descriptor();
532        let callback = descriptor.callback.unwrap();
533        let url = CString::new("https://example.test/style.json").unwrap();
534        let mut response = response();
535
536        let status = unsafe {
537            callback(
538                descriptor.user_data,
539                sys::MLN_RESOURCE_KIND_STYLE,
540                url.as_ptr(),
541                &mut response,
542            )
543        };
544
545        assert_eq!(status, sys::MLN_STATUS_OK);
546        assert!(response.url.is_null());
547    }
548
549    #[test]
550    fn transform_callback_clears_stale_response_when_keeping_original_url() {
551        let state = ResourceTransformState::new(|_| None);
552        let descriptor = state.descriptor();
553        let callback = descriptor.callback.unwrap();
554        let url = CString::new("https://example.test/style.json").unwrap();
555        let stale = CString::new("https://stale.test/style.json").unwrap();
556        let mut response = response();
557        response.url = stale.as_ptr();
558
559        let status = unsafe {
560            callback(
561                descriptor.user_data,
562                sys::MLN_RESOURCE_KIND_STYLE,
563                url.as_ptr(),
564                &mut response,
565            )
566        };
567
568        assert_eq!(status, sys::MLN_STATUS_OK);
569        assert!(response.url.is_null());
570    }
571
572    #[test]
573    fn transform_callback_contains_panics() {
574        let state = ResourceTransformState::new(|_| panic!("boom"));
575        let descriptor = state.descriptor();
576        let callback = descriptor.callback.unwrap();
577        let url = CString::new("https://example.test/style.json").unwrap();
578        let mut response = response();
579
580        let status = unsafe {
581            callback(
582                descriptor.user_data,
583                sys::MLN_RESOURCE_KIND_STYLE,
584                url.as_ptr(),
585                &mut response,
586            )
587        };
588
589        assert_eq!(status, sys::MLN_STATUS_NATIVE_ERROR);
590        assert!(response.url.is_null());
591    }
592
593    #[test]
594    fn transform_callback_rejects_embedded_nul_replacements() {
595        let state = ResourceTransformState::new(|_| Some("https://example.test/\0bad".to_owned()));
596        let descriptor = state.descriptor();
597        let callback = descriptor.callback.unwrap();
598        let url = CString::new("https://example.test/style.json").unwrap();
599        let mut response = response();
600
601        let status = unsafe {
602            callback(
603                descriptor.user_data,
604                sys::MLN_RESOURCE_KIND_STYLE,
605                url.as_ptr(),
606                &mut response,
607            )
608        };
609
610        assert_eq!(status, sys::MLN_STATUS_INVALID_ARGUMENT);
611        assert!(response.url.is_null());
612    }
613
614    #[test]
615    fn transform_state_drops_callback_capture() {
616        let token = Arc::new(());
617        let callback_token = Arc::clone(&token);
618        let state = ResourceTransformState::new(move |_| {
619            let _ = &callback_token;
620            None
621        });
622        assert_eq!(Arc::strong_count(&token), 2);
623        drop(state);
624        assert_eq!(Arc::strong_count(&token), 1);
625    }
626
627    #[test]
628    fn callback_can_run_from_multiple_threads() {
629        let calls = Arc::new(AtomicUsize::new(0));
630        let callback_calls = Arc::clone(&calls);
631        let state = Arc::new(ResourceTransformState {
632            callback: Box::new(move |request| {
633                callback_calls.fetch_add(1, Ordering::SeqCst);
634                let _ = request;
635                None
636            }),
637        });
638
639        let handles = (0..2)
640            .map(|_| {
641                let state = Arc::clone(&state);
642                std::thread::spawn(move || {
643                    let descriptor = state.descriptor();
644                    let callback = descriptor.callback.unwrap();
645                    let url = CString::new("https://example.test/tile").unwrap();
646                    let mut response = response();
647                    let status = unsafe {
648                        callback(
649                            descriptor.user_data,
650                            sys::MLN_RESOURCE_KIND_TILE,
651                            url.as_ptr(),
652                            &mut response,
653                        )
654                    };
655                    assert_eq!(status, sys::MLN_STATUS_OK);
656                    assert!(response.url.is_null());
657                })
658            })
659            .collect::<Vec<_>>();
660
661        for handle in handles {
662            handle.join().unwrap();
663        }
664        assert_eq!(calls.load(Ordering::SeqCst), 2);
665    }
666}