Terrain-DEM tiles that arrive already expired are re-downloaded in full every 1, 2, 4… 64 s while visible
Nessuno ha ancora preso questa issue.
Valutazione
- Difficoltà
- 4/5
- Tempo stimato
- 3-5 giorni
- Idoneità per principianti
- 48/100
- Tipo di issue
- Bug
- Chiarezza
- Abbastanza chiara
- Stato di attività
- Attiva
- Ambito
- mobile-dev
Direzione di ricerca
Start with the Android Maps SDK terrain DEM path and the MainActivity.kt reproduction using SIMULATED_CDN_AGE; compare its behavior with the expired-response backoff described in mapbox/mapbox-gl-native#4019. Determine whether the client cache or the response headers account for the repeated downloads, then verify that visible expired DEM tiles no longer trigger repeated full 200 responses.
Scritto dal modello di indicizzazione a partire dal testo della issue.
Descrizione
Environment
- Android OS version: 17
- Devices affected: Pixel 6a, Android emulator
- Maps SDK Version: 11.30.0 and 11.32.0
Observed behavior and steps to reproduce
Terrain-DEM tiles (https://api.mapbox.com/raster/v1/mapbox.mapbox-terrain-dem-v1/{z}/{x}/{y}.png) are served with cache-control: max-age=43200,s-maxage=2592000 and no ETag or Last-Modified. So once a tile has been in a CloudFront edge cache for more than 12 hours, every device that gets that copy receives a tile that is already expired. The SDK then downloads it again in full, roughly 100 KB each time, at +1, 2, 4, 8, 16, 32, 64 s… for as long as it stays on screen. The camera doesn't move.
Steps, using the Use 3D terrain example with a fixed camera (zoom 17, pitch 45) and the logging activity below:
- Open it over a city centre. The DEM tiles are CloudFront misses (no
Age) and cache normally. - At least 12 hours later, clear app data and open it again at the same place, on the same network. The same tiles now come back as CloudFront hits with
Age> 43200 and are re-downloaded indefinitely.
We did exactly this on 2 October 2026 (POP FRA56-P13). At 00:34 UTC Munich 14/8718/5685 was a miss. At 14:28 UTC the same tile (tokens removed):
14:28:53.170 200 102869B age=50075 x-cache=Hit_from_cloudfront cache-control=max-age=43200,s-maxage=2592000 etag=null last-modified=null
14:28:54.371 200 102869B age=50076 …
14:28:56.404 200 102869B age=50079 …
14:29:00.469 200 102869B age=50083 …
14:29:08.540 200 102869B age=50091 …
14:29:24.655 200 102869B age=50107 …
In a 24-location sweep that afternoon, all 7 locations first loaded about 14 hours earlier looped: Berlin at zoom 12 and 17, London, Munich, Paris, New York and one other city. All 128 of their DEM responses arrived expired. All 17 locations not loaded before were fresh misses. The behaviour is the same on 11.30.0 and 11.32.0.
To reproduce without waiting 12 hours, set SIMULATED_CDN_AGE in the activity. It rewrites Date and Age on DEM responses to look like a hit on a 50,000 s-old copy. Berlin then shows the same loop: each of the 4 visible DEM tiles is downloaded 8 times in about 2 minutes, 32 full 200 responses, about 3 MB.
MainActivity.kt (Maps SDK 11.32.0, com.mapbox.maps:android-ndk27)
package com.example.demrepro
import android.os.Bundle
import android.util.Log
import androidx.activity.ComponentActivity
import com.mapbox.bindgen.ExpectedFactory
import com.mapbox.common.HttpRequest
import com.mapbox.common.HttpRequestOrResponse
import com.mapbox.common.HttpResponse
import com.mapbox.common.HttpResponseData
import com.mapbox.common.HttpServiceFactory
import com.mapbox.common.HttpServiceInterceptorInterface
import com.mapbox.common.HttpServiceInterceptorRequestContinuation
import com.mapbox.common.HttpServiceInterceptorResponseContinuation
import com.mapbox.geojson.Point
import com.mapbox.maps.CameraOptions
import com.mapbox.maps.MapInitOptions
import com.mapbox.maps.MapView
import com.mapbox.maps.Style
import com.mapbox.maps.extension.style.sources.generated.rasterDemSource
import com.mapbox.maps.extension.style.style
import com.mapbox.maps.extension.style.terrain.generated.terrain
import java.time.Instant
import java.time.ZoneOffset
import java.time.format.DateTimeFormatter
import java.util.Locale
// The "Use 3D terrain" example with a fixed street-level camera, logging every terrain-DEM response.
// SIMULATED_CDN_AGE > 0 makes DEM responses look like a CloudFront hit on a copy that many seconds old
// (Date fixed at the copy's time, Age growing), so the loop shows without waiting 12 hours.
class MainActivity : ComponentActivity() {
private val copyTimes = mutableMapOf<String, Long>()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
HttpServiceFactory.setHttpServiceInterceptor(DemLogger())
val camera = CameraOptions.Builder().center(Point.fromLngLat(13.4050, 52.5200)).zoom(17.0).pitch(45.0).build()
val mapView = MapView(this, MapInitOptions(this, cameraOptions = camera))
setContentView(mapView)
mapView.mapboxMap.loadStyle(
style(Style.STANDARD_SATELLITE) {
+rasterDemSource("TERRAIN_SOURCE") {
url("mapbox://mapbox.mapbox-terrain-dem-v1")
tileSize(514)
}
+terrain("TERRAIN_SOURCE") { exaggeration(1.5) }
},
)
}
private inner class DemLogger : HttpServiceInterceptorInterface {
override fun onRequest(request: HttpRequest, continuation: HttpServiceInterceptorRequestContinuation) =
continuation.run(HttpRequestOrResponse(request))
override fun onResponse(response: HttpResponse, continuation: HttpServiceInterceptorResponseContinuation) {
val data = response.result.value
val path = response.request.url.substringBefore('?')
if ("terrain-dem-v1/" !in path || data == null) return continuation.run(response)
val headers = HashMap(data.headers)
if (SIMULATED_CDN_AGE > 0) {
val now = System.currentTimeMillis() / 1000
val copyTime = copyTimes.getOrPut(path) { now - SIMULATED_CDN_AGE }
headers["age"] = (now - copyTime).toString()
headers["date"] = HTTP_DATE.format(Instant.ofEpochSecond(copyTime))
}
val h = headers.mapKeys { it.key.lowercase() }
Log.i(TAG, "${path.substringAfter("terrain-dem-v1/")} ${data.code} ${data.data.size}B age=${h["age"]} cache-control=${h["cache-control"]} etag=${h["etag"]} last-modified=${h["last-modified"]}")
continuation.run(HttpResponse(response.requestId, response.request, ExpectedFactory.createValue(HttpResponseData(headers, data.code, data.data))))
}
}
companion object {
const val TAG = "DemRepro"
const val SIMULATED_CDN_AGE = 50_000L // seconds; 0 = real headers only
val HTTP_DATE: DateTimeFormatter = DateTimeFormatter.ofPattern("EEE, dd MMM yyyy HH:mm:ss 'GMT'", Locale.US).withZone(ZoneOffset.UTC)
}
}
Log with SIMULATED_CDN_AGE = 50_000, Berlin (one line per DEM response):
03:39:25.328 14/8801/5373.png 200 102803B age=50000 cache-control=max-age=43200,s-maxage=2592000 etag=null last-modified=null
03:39:26.354 14/8801/5373.png 200 102803B age=50001 …
03:39:28.430 14/8801/5373.png 200 102803B age=50003 …
03:39:32.616 14/8801/5373.png 200 102803B age=50007 …
03:39:40.688 14/8801/5373.png 200 102803B age=50015 …
03:39:56.791 14/8801/5373.png 200 102803B age=50031 …
03:40:29.488 14/8801/5373.png 200 102803B age=50064 …
03:41:34.339 14/8801/5373.png 200 102803B age=50129 …
(same for 14/8802/5372, 14/8801/5372, 14/8802/5373)
Impact in our cycling app, which has a moving, pitched navigation camera: repeating the same 4.1 km route used 9.0–9.6 MB per ride, nearly all of it DEM tiles fetched up to 31 times each. Vector tiles on that route were served from the disk cache.
Expected behavior
Each DEM tile is downloaded once and then served from the disk cache, or revalidated with a 304, like vector tiles. The cache management guide says an unchanged resource gets "a 'Not Modified' response … the data payload is not sent". The Raster Tiles API documents max-age=43200,s-maxage=300. There the CDN lifetime is shorter than the device lifetime, so a CDN copy can never arrive expired.
Notes / preliminary analysis
- Server side.
s-maxage(30 days) is far abovemax-age(12 hours), and there are no validators. A CloudFront copy older than 12 h is served already expired and can't be revalidated. Possible fixes:s-maxage≤max-agefor this endpoint, and/or anETag/Last-Modifiedso a refresh is a 304. - Client side.
- The SDK stores expiry as receipt time +
max-age−Age. That is identical on every refetch of the same CDN copy, so it ends up in the expired-retry backoff (expirationTimeout, mapbox/mapbox-gl-native#4019), which never stops while the tile is visible. - Vector and satellite tiles go through the same refetches but carry ETags, so each costs a 0-byte
304. - Not refetching responses that arrive expired with no validators, or capping it, would contain this.
- The SDK stores expiry as receipt time +
minimumTileUpdateInterval(86_400.0)on the raster-dem source only partly helps.- When the tile was already in the disk cache, it was fetched once and its expiry set to +86399 s. That took our repeated route from 9.0–9.6 MB to 0.54 MB.
- A tile first fetched in the current session still loops at 1, 2, 4… s with the property set.
- What works in both cases: removing
Agefrom DEM responses in anHttpServiceInterceptor. Each tile is then fetched once.
- Scope.
- Standard fades terrain out (exaggeration 0 from zoom 13.7), which disables it at street level, so the stock Standard style should only be exposed from zoom 7 to 13.7.
- Styles that keep terrain or hillshade on at high zoom, like the example above, are exposed at every zoom. The example loops the same way at zoom 12.
- iOS uses the same tiles; not tested.
Additional links and references
- mapbox/mapbox-gl-native#4019 (backoff for responses that arrive expired, "otherwise a faulty server can trigger an endless number of consecutive requests") and mapbox/mapbox-gl-js#4345 (same kind of loop for
Expiresin the past)
- Lingua principale
- Kotlin
- Stelle
- 578
- Fork
- 163
- Metriche di merge delle PR
- Nessuna PR unita negli ultimi 30g
Preparare l'ambiente
- Nessun Dockerfile né file Docker Compose
- Ha un modello di pull request
- Leggi la guida per i contributori
Come iniziare
- Leggi tutta la issue e poi la guida ai contributi del progetto.
- Commenta sulla issue per dire che te ne occupi tu — evita che due persone facciano lo stesso lavoro.
- Fai un fork del repository e lavora su un branch.
- Apri una pull request che faccia riferimento al numero della issue.
Altre issue di mapbox/mapbox-maps-android
-
bug :beetle:
Difficoltà 2/5 1-3 ore Idoneità per principianti 76/100
mapbox/mapbox-maps-android#2727 ·
-
bug :beetle:
Difficoltà 5/5 Più di una settimana Idoneità per principianti 22/100
mapbox/mapbox-maps-android#2734 · 1 reazione ·
-
Pins Flicker During Zoom OutApertabug :beetle:
Difficoltà 4/5 3-5 giorni Idoneità per principianti 50/100
mapbox/mapbox-maps-android#2732 ·
-
[R8] Maps and Search SDK consumer rules apply app-wide and significantly restrict optimizationApertaauto-triaged bug :beetle:
Difficoltà 4/5 3-5 giorni Idoneità per principianti 45/100
mapbox/mapbox-maps-android#2731 · 4 reazioni ·
-
bug :beetle:
Difficoltà 4/5 3-5 giorni Idoneità per principianti 48/100
mapbox/mapbox-maps-android#2730 ·
Tutte le issue di mapbox/mapbox-maps-android
Issue simili
-
afk-ok area:data-quality importer size:S
Difficoltà 2/5 1-3 ore Idoneità per principianti 82/100
enorm-labs/event-junkie#3027 ·
I maintainer di solito rispondono entro 1 giorno
-
Difficoltà 2/5 1-3 ore Idoneità per principianti 72/100
-
Difficoltà 2/5 1-3 ore Idoneità per principianti 62/100
I maintainer di solito rispondono entro 1 giorno
-
bug
Difficoltà 2/5 1-3 ore Idoneità per principianti 68/100
libre-tube/LibreTube#8854 ·
I maintainer di solito rispondono entro 1 giorno
-
enhancement
Difficoltà 2/5 1-3 ore Idoneità per principianti 72/100
rustyrazorblade/easy-db-lab#1003 ·
I maintainer di solito rispondono entro 1 giorno