Local CORS proxy: nginx forwards /carto/ to a.basemaps.cartocdn.com

Direct fetching from a.basemaps.cartocdn.com was returning
stripped/missing CORS headers in the user's browser, blocking
the img.src load. Even with cache-busters the failure persisted.

Solution: local nginx proxy at /carto/ that forwards to the
upstream and always adds Access-Control-Allow-Origin: *.

Key detail: the 'resolver' directive is required inside the
proxy_pass location block so nginx can resolve the upstream
hostname. Without it, the location block silently fails (the
'/' location falls through to try_files ... =404, and the
~*\.(png|jpg)$ regex also matches and silently fails for GETs).

Also added 'location ^~ /carto/' to give prefix matching strict
priority over the .png regex match that was stealing the request.

JS now uses /carto/voyager/ (relative URL through the same
origin) so CORS preflight is not even needed.
This commit is contained in:
Richard Zink committed 2026-10-08 08:57:33 +02:00
1 parent 83fd829cd5
commit f55d96f462
2 files changed
+31 -7

No files matched your search

+28 -1
View File
@@ -1,10 +1,37 @@
server {
listen 58881 default_server;
server_name localhost;
server_name localhost _;
root /usr/share/nginx/html;
index background.html;
location / {
try_files $uri $uri/ =404;
}
# CORS proxy for map tiles. Forwards to the upstream tile server
# but injects permissive CORS headers so the browser can load
# tiles via cross-origin img.src requests. The 'resolver' is
# required by the proxy module to resolve the upstream hostname.
location ^~ /carto/ {
resolver 1.1.1.1 8.8.8.8 ipv6=off valid=300s;
proxy_pass https://a.basemaps.cartocdn.com/;
proxy_ssl_server_name on;
proxy_set_header Host a.basemaps.cartocdn.com;
proxy_set_header User-Agent "Mozilla/5.0 (compatible; tile-proxy)";
add_header Access-Control-Allow-Origin "*" always;
add_header Access-Control-Allow-Methods "GET, HEAD, OPTIONS" always;
add_header Access-Control-Max-Age 86400 always;
}
# CORS preflight handler
location ~* \.(png|jpg)$ {
if ($request_method = 'OPTIONS') {
add_header Access-Control-Allow-Origin "*" always;
add_header Access-Control-Allow-Methods "GET, HEAD, OPTIONS" always;
add_header Access-Control-Max-Age 86400 always;
add_header Content-Length 0;
return 204;
}
add_header Access-Control-Allow-Origin "*" always;
}
}
+3 -6
View File
@@ -284,7 +284,7 @@ function init(canvas) {
// mosaic centre always corresponds to UV (0.5, 0.5).
// Tile source: Carto basemaps Voyager style (with place names).
// Free, CORS-enabled, OSM-derived. URL format {z}/{x}/{y}.png.
var GROUND_TILE_URL = 'https://a.basemaps.cartocdn.com/voyager/';
var GROUND_TILE_URL = '/carto/voyager/';
var GROUND_TILE_Z = 11;
var GROUND_TILE_SIZE = 256;
// Fetch direction: which way the tile corridor is oriented.
@@ -318,11 +318,8 @@ function fetchTileImage(tileX, tileY) {
img.crossOrigin = 'anonymous';
img.onload = function() { resolve(img); };
img.onerror = function() { reject(new Error('tile fetch failed')); };
// Carto/OSM-style format: {z}/{x}/{y}.png with a per-tile cache
// buster to avoid any browser-cached 404s or CORS rejections
// from previous failed loads.
img.src = GROUND_TILE_URL + GROUND_TILE_Z + '/' + tileX + '/' + tileY
+ '?v=' + Date.now() + '_' + Math.floor(Math.random() * 1e6);
// Carto/OSM-style format: {z}/{x}/{y}.png via local proxy
img.src = GROUND_TILE_URL + GROUND_TILE_Z + '/' + tileX + '/' + tileY;
});
}