-
Notifications
You must be signed in to change notification settings - Fork 2.3k
Description
mapbox-gl-js version
3.18.1
Browser and version
Chrome 145.0.7632.76
Expected behavior
After the max-age duration expires, mapbox-gl should automatically re-request the visible tiles, matching the behavior in v2.x.
Actual behavior
Description
Vector tile sources with Cache-Control: max-age=N response headers no longer auto-refresh after tile expiry. The refreshExpiredTiles mechanism is completely broken for all non-Mapbox tile sources since v3.0.0.
Root Cause
In src/util/ajax.js, the getExpiryDataFromHeaders function reads headers using title-case keys:
function getExpiryDataFromHeaders(responseHeaders) {
const cacheControl = responseHeaders.get('Cache-Control');
const expires = responseHeaders.get('Expires');
return { cacheControl, expires };
}This works when responseHeaders is a Fetch API Headers object (which is case-insensitive per the Fetch spec).
However, in the vector tile worker pipeline (src/source/vector_tile_worker_source.js → loadVectorTile), the Headers object is serialized into a JavaScript Map before being sent back from the worker:
responseHeaders: new Map(responseHeaders.entries())Per the Fetch specification, Headers.entries() yields lowercase header names. So the Map contains keys like 'cache-control' and 'expires'.
When getExpiryDataFromHeaders later calls Map.get('Cache-Control'), this returns undefined because Map.get() is case-sensitive — unlike Headers.get().
Impact
The entire tile refresh chain is broken:
getExpiryDataFromHeaders()→ returns{ cacheControl: undefined, expires: undefined }Tile.setExpiryData()→ no-op (both values undefined)Tile.expirationTime→ never setTile.getExpiryTimeout()→ returnsundefinedSourceCache._setTileReloadTimer()→ never creates asetTimeout- Tiles are never automatically re-fetched, even though
refreshExpiredTilesdefaults totrue
This affects all custom vector tile sources (MVT, GeoJSON served as vector tiles, etc.) that rely on Cache-Control or Expires response headers for automatic refresh. Mapbox's own tiles are not affected because they use a separate internal cache mechanism with SKU-based cache defeating (cacheIgnoringSearch / hasCacheDefeatingSku).
Suggested Fix
Change the header key lookups in getExpiryDataFromHeaders to use lowercase keys, matching what Headers.entries() produces:
function getExpiryDataFromHeaders(responseHeaders) {
const cacheControl = responseHeaders.get('cache-control');
const expires = responseHeaders.get('expires');
return { cacheControl, expires };
}This is safe because:
- When
responseHeadersis aHeadersobject (Fetch API),get()is case-insensitive — lowercase works fine - When
responseHeadersis aMap(after worker serialization),get()is case-sensitive — lowercase matches the keys fromHeaders.entries()
Alternatively, use a case-insensitive lookup helper, or convert the Map back to a Headers object on the main thread.
The same case-sensitivity issue also applies to the two other Cache-Control lookups in ajax.js related to the cacheOpen/cachePut service worker cache logic, though those operate on Headers objects directly and are not currently broken (they would be if ever passed a Map).
Related Code Paths
src/util/ajax.js→getExpiryDataFromHeaders()— the buggy functionsrc/source/vector_tile_worker_source.js→loadVectorTile()—new Map(responseHeaders.entries())serializationsrc/source/vector_tile_source.js→loadTiledone callback →tile.setExpiryData()src/source/source_cache.js→_setTileReloadTimer()— the timer that should trigger re-fetchsrc/source/tile.js→setExpiryData()/getExpiryTimeout()— expiration time calculation
Link to the demonstration
No response
Steps to trigger the unexpected behavior
- Set up a vector tile source (MVT) with a server that returns
Cache-Control: max-age=20response headers - Add the source to a map:
<Source type="vector" tiles={["https://example.com/tiles/{z}/{x}/{y}.mvt"]} />
- Wait more than 20 seconds
- Observe: tiles are never re-requested despite cache expiry
- In v2.x with the same setup, tiles were automatically re-fetched every 20 seconds