Skip to content

Intercept network requests

Three extension points intercept resource requests. A resource transform rewrites a URL before the network request. An HTTP header transform adds headers to the request that follows. A resource provider supplies the response bytes. Register each one on the runtime owner thread.

MapLibre can invoke any of these callbacks concurrently on network and worker threads. Protect every value that a callback reads and return quickly because the resource request waits. A transform callback sets a replacement URL. A header transform callback sets header names and values. A provider callback operates only on the request handle that it received.

Rewrite a URL to add a credential or signature, or to select another host. The transform runs for every network resource, including nested PMTiles range requests. File, asset, MBTiles, and cache-database resources bypass it.

Compare the URL against your own host before you attach a credential, because a style names glyphs, sprites, and tiles on origins that you do not control.

resource-transform.c
const char* api_key = user_data;
(void)kind;
if (strncmp(url, trusted_prefix, sizeof(trusted_prefix) - 1) != 0) {
return MLN_STATUS_OK; // An empty response preserves the original URL.
}

Build the replacement URL in host storage, then set it on the response. Returning a non-OK status preserves the original request.

resource-transform.c
char rewritten[2048];
if (!build_keyed_url(url, api_key, rewritten, sizeof(rewritten))) {
return MLN_STATUS_OK;
}
// The helper copies the URL before rewritten leaves scope.
return mln_resource_transform_response_set_url(
out_response, rewritten, strlen(rewritten)
);

Registering a transform replaces the previous transform. After the registration call returns, the replaced transform has no in-flight callbacks.

resource-transform.c
void install_transform(mln_runtime runtime, char* api_key) {
mln_resource_transform transform = {
.size = sizeof(transform),
.callback = add_api_key,
.user_data = api_key,
};
mln_runtime_set_resource_transform(runtime, &transform);
}

Add a header to carry a bearer token or another credential that belongs in the request rather than in the URL. A header transform runs after URL transformation and immediately before the HTTP client sends the request. It reaches online and offline requests, including nested PMTiles range requests. Cache hits, non-HTTP schemes, and requests that a provider answered bypass it.

Compare the transformed URL against your own origin before you return a credential, because the callback runs for every network resource.

http-header-transform.c
const char* token = user_data;
(void)kind;
if (strncmp(url, trusted_prefix, sizeof(trusted_prefix) - 1) != 0) {
return MLN_STATUS_OK; // Every other origin gets no header.
}

Set each name and value on the response. Returning a non-OK status discards the headers from that invocation and sends the request unchanged.

http-header-transform.c
// The helper copies the name and the value before it returns.
return mln_http_header_transform_response_set(
out_response, header_name, sizeof(header_name) - 1, token, strlen(token)
);

Register the transform on the runtime, and use mln_runtime_clear_http_header_transform to stop adding headers. Both calls work while maps are live, so a host installs a refreshed credential without recreating the runtime.

http-header-transform.c
void install_header_transform(mln_runtime runtime, char* token) {
mln_http_header_transform transform = {
.size = sizeof(transform),
.callback = add_authorization,
.user_data = token,
};
mln_runtime_set_http_header_transform(runtime, &transform);
}

A redirect that keeps the scheme, host, and effective port carries the headers to its destination. A redirect that changes any of the three removes every header that the transform added, and it invokes no transform for the destination. Registration returns an unsupported status on OpenHarmony and in the browser, whose HTTP clients cannot make that distinction. Serve those requests with a provider instead, as the next section shows.

Use a provider for host-managed storage, such as an application archive or a custom URL scheme. MapLibre routes unrecognized schemes to the network layer, where the provider runs before the online file source. A provider response bypasses the transform.

Match the requests that the host serves, and pass every other request to MapLibre’s network stack. Fetch the resolved URL after tile-server normalization and API-key insertion. The cache uses the requested URL as the logical identity.

resource-provider.c
const asset_store* store = user_data;
const uint8_t* bytes = NULL;
size_t size = 0;
if (
strncmp(request->resolved_url, "app://", 6) != 0 ||
!asset_store_find(store, request->resolved_url, &bytes, &size)
) {
return MLN_RESOURCE_PROVIDER_DECISION_PASS_THROUGH;
}

A provider with the bytes available completes the request inside the callback, releases the handle, and reports that it handled the request.

resource-provider.c
const mln_resource_response response = {
.size = sizeof(response),
.status = MLN_RESOURCE_RESPONSE_STATUS_OK,
.error_reason = MLN_RESOURCE_ERROR_REASON_NONE,
.bytes = bytes,
.byte_count = size,
};
// The C API copies these bytes before the call returns.
mln_resource_request_complete(handle, &response);
mln_resource_request_release(handle);
return MLN_RESOURCE_PROVIDER_DECISION_HANDLE;

Registering a provider replaces any previous one. Handles that the previous provider took stay valid. Complete and release each of them as usual.

resource-provider.c
void install_provider(mln_runtime runtime, asset_store* store) {
const mln_resource_provider provider = {
.size = sizeof(provider),
.callback = serve_bundled_asset,
.user_data = store,
};
mln_runtime_set_resource_provider(runtime, &provider);
}

A provider with an I/O pool reports the same decision and retains the handle. Complete the request from any thread when the bytes arrive. Release each handle exactly once, after completion or cancellation.

MapLibre cancels a request when the map stops wanting the resource, for example after a style change or map teardown. Check for cancellation before expensive work, or register a cancel callback to abort an in-flight fetch. A request takes one callback. It runs once, for a request the provider has not completed, on the thread that discards the request. That is the owner thread inside a map call such as a style change or map destruction, and a MapLibre thread otherwise. The callback may complete or release the same handle. A request that was cancelled before the registration reports that through the out-parameter instead, and the callback never runs.

resource-provider.c
typedef struct pending_fetch pending_fetch;
// The host's I/O pool aborts the fetch. Its completion path then releases the
// handle exactly once, whether the fetch finished or was aborted.
void pending_fetch_abort(pending_fetch* fetch);
static void abort_pending_fetch(void* user_data) {
pending_fetch_abort(user_data);
}
// Registers the abort hook for a request the provider retained. MapLibre runs
// the hook on one of its threads when the map stops wanting the resource. A
// request that was cancelled before the registration reports that instead.
void watch_for_cancellation(
mln_resource_request_handle handle, pending_fetch* fetch
) {
bool already_cancelled = false;
if (
mln_resource_request_set_cancel_callback(
handle, abort_pending_fetch, fetch, &already_cancelled
) == MLN_STATUS_OK &&
already_cancelled
) {
pending_fetch_abort(fetch);
}
}

The runtime stores each callback and its user data by reference. Keep callback state reachable until replacing or clearing the callback, or destroying the runtime. Each operation waits for in-flight callbacks to return. Release any host lock that a callback also acquires before destroying the runtime. Holding that lock can deadlock destruction.