Hacktoberfest 2026: le issue che i maintainer hanno segnato per ottobre, aperte e adatte ai principianti. Sfoglia le issue Hacktoberfest

Terrain-DEM tiles that arrive already expired are re-downloaded in full every 1, 2, 4… 64 s while visible

Aperta
#2,733 0 commenti 0 reazioni 0 assegnatari Vedi su GitHub

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
Stack tecnologico
android, kotlin
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:

  1. Open it over a city centre. The DEM tiles are CloudFront misses (no Age) and cache normally.
  2. 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 above max-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-age for this endpoint, and/or an ETag/Last-Modified so 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.
  • 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 Age from DEM responses in an HttpServiceInterceptor. 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 Expires in the past)
Lingua principale
Kotlin
Stelle
578
Fork
163
Metriche di merge delle PR
Nessuna PR unita negli ultimi 30g

Preparare l'ambiente

Come iniziare

  1. Leggi tutta la issue e poi la guida ai contributi del progetto.
  2. Commenta sulla issue per dire che te ne occupi tu — evita che due persone facciano lo stesso lavoro.
  3. Fai un fork del repository e lavora su un branch.
  4. Apri una pull request che faccia riferimento al numero della issue.

Altre issue di mapbox/mapbox-maps-android

Tutte le issue di mapbox/mapbox-maps-android

Issue simili

Altre issue su Kotlin

Ricevi le nuove issue nella tua casella

Un breve riepilogo di issue GitHub adatte ai principianti.