429 Too Many Requests

The addon map allows to use wms from external source.
But in some cases, the user, by moving or zooming, does too many requests on the WMS endpoint.
OpenLayers get a “429 Too Many Requests” and the tule is not displayed

image

But often the solution is just to call again after a few time.
https://openlayers.org/en/latest/apidoc/module-ol_ImageTile-ImageTile.html

How can I add this code in my app ?
Or maybe could it be added in jmix ?

Hello!

I’ve write a code that sets this function to a WMS source. Could you check that this code resolves the issue? If it works, I would create a GitHub issue to provide the ability to add and configure this function from Maps.

@Subscribe
public void onReady(ReadyEvent event) {
    map.getElement().executeJs("""
            setTimeout(() => {
                const layers = this.olMap.getLayers().getArray();
                for (const layer of layers) {
                    if (layer.constructor.name !== 'TileLayer' || !layer.getSource()) {
                        continue;
                    }
                    const source = layer.getSource();
                    if (source.constructor.name !== 'TileWMS') {
                        continue;
                    }
                    const retryCodes = [408, 429, 500, 502, 503, 504];
                    const retries = {};
                    source.setTileLoadFunction((tile, src) => {
                        const image = tile.getImage();
                        fetch(src)
                          .then((response) => {
                            if (retryCodes.includes(response.status)) {
                              retries[src] = (retries[src] || 0) + 1;
                              if (retries[src] <= 3) {
                                setTimeout(() => tile.load(), retries[src] * 1000);
                              }
                              return Promise.reject();
                            }
                            return response.blob();
                          })
                          .then((blob) => {
                            const imageUrl = URL.createObjectURL(blob);
                            image.src = imageUrl;
                            setTimeout(() => URL.revokeObjectURL(imageUrl), 5000);
                          })
                          .catch(() => tile.setState(3)); // error
                    });
                }
            } , 0);
            """);
}

Thanks! I tested your snippet and it does help, but the issue still persists on our side.
The main cause seems to be that Promise.reject() flows into .catch(() => tile.setState(3)), so the tile is marked as error during the retry phase. Also, three retries is often too few under throttling.
I switched to keeping retries going and honoring Retry-After from the response headers (seconds or HTTP-date); otherwise I use a small capped backoff.
map.element.executeJs("""
setTimeout(() => {
const layers = this.olMap.getLayers().getArray();
for (const layer of layers) {
if (layer.constructor.name !== ‘TileLayer’ || !layer.getSource()) continue;
const source = layer.getSource();
if (source.constructor.name !== ‘TileWMS’) continue;
const retryCodes = [408, 429, 500, 502, 503, 504];
const MAX_BACKOFF_MS = 15000;
const BASE_BACKOFF_MS = 800;
const sleep = (ms) => new Promise(r => setTimeout(r, ms));
source.setTileLoadFunction((tile, src) => {
const image = tile.getImage();
let attempt = 0;
(async function loadForever() {
while (true) {
attempt++;
try {
const resp = await fetch(src);
if (retryCodes.includes(resp.status)) {
const ra = resp.headers.get(‘Retry-After’);
const raMs = ra
? (isNaN(+ra) ? Math.max(0, Date.parse(ra) - Date.now()) : (+ra * 1000))
: Math.min(BASE_BACKOFF_MS * attempt, MAX_BACKOFF_MS);
await sleep(raMs);
continue;
}
const blob = await resp.blob();
const url = URL.createObjectURL(blob);
image.src = url;
setTimeout(() => URL.revokeObjectURL(url), 5000);
break;
} catch (e) {
const delay = Math.min(BASE_BACKOFF_MS * attempt, MAX_BACKOFF_MS);
await sleep(delay);
}
}
})();
});
}
} , 0);
“”");

1 Like

I’ve created a GitHub issue to set this function by default with ability to replace the function by custom one: Provide ability to set tile load function to UrlTile based sources · Issue #4818 · jmix-framework/jmix · GitHub

1 Like

Hello,
I modified my function to add a throttling mechanism and a retry queue for WMS tiles.
Everything works correctly in development mode, but fails in production mode.
After investigating, I found that in production constructor.name is minified, so class names like TileLayer and TileWMS are replaced by minified identifiers.
What is the recommended way to detect a TileLayer with a TileWMS source in production mode? Thank you!

private fun applyWMSTileRetry(map: GeoMap, layerDelays: String): PendingJavaScriptResult? = map.element.executeJs(
“”"
setTimeout(() => {
const layers = this.olMap.getLayers().getArray();
layers.forEach(layer => {
const source = layer.getSource();
if (layer.constructor.name === ‘TileLayer’ && source && source.constructor.name === ‘TileWMS’) {
source.refresh();
// File d’attente pour les tuiles
const pendingCalls = [];
let isProcessing = false;
let lastCallTime = 0;
const layerUrl = source.getUrls()[0];
const layerDelays = JSON.parse($0);
const DELAY = layerDelays[layerUrl] || 0;
const MAX_RETRIES = 10;

          async function processQueue() {
            if (isProcessing || pendingCalls.length === 0) return;
            isProcessing = true;
            while (pendingCalls.length > 0) {
              const now = performance.now();
              const timeSinceLastCall = now - lastCallTime;
              if (timeSinceLastCall < DELAY) {
                await new Promise(resolve => setTimeout(resolve, DELAY - timeSinceLastCall));
              }
              lastCallTime = performance.now();
              const item = pendingCalls.shift(); 
              // Charger la tuile avec retry
              const success = await loadTile(item.tile, item.src);
              if (!success && item.retryCount < MAX_RETRIES) {              
                item.retryCount++;
                pendingCalls.push(item);  
              }
            }
            isProcessing = false;
          } 
          async function loadTile(tile, src) {
            const image = tile.getImage();
            try {
              const resp = await fetch(src);
              if (resp.ok) {
                const blob = await resp.blob();
                const url = URL.createObjectURL(blob);
                image.src = url;
                image.onload = () => {
                  setTimeout(() => URL.revokeObjectURL(url), 5000);
                };
                image.onerror = () => {
                  URL.revokeObjectURL(url);
                };
                return true; // Succès
              } else {
                console.log(`Failed to load tile: ${'$'}{src}, status: ${'$'}{resp.status}`);
                return false; echec - réessayé
              }
            } catch (error) {
              console.log("Error loading tile:", error);
              return false; // echec - réessayé
            }
          } 
          source.setTileLoadFunction((tile, src) => {
              pendingCalls.push({ tile, src, retryCount: 0 });
              processQueue(); // Continuer à traiter la file d'attente
          });
        }
      });
    }, 0);
    """, layerDelays
)

Hello!

In this case, you can try to detect the source type indirectly. For instance, check specific TileWMS properties:

  • TileWMS has its own methods: getParams() and updateParams().
  • Additionally check the URL using source.getUrls().