From f55d96f462f11197714e2aa4c2d47f24706030d3 Mon Sep 17 00:00:00 2001 From: Richard Zink Date: Thu, 8 Oct 2026 08:57:33 +0200 Subject: [PATCH] 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. --- default.conf | 31 +++++++++++++++++++++++++++++-- vendor/clouds3d.js | 9 +++------ 2 files changed, 32 insertions(+), 8 deletions(-) diff --git a/default.conf b/default.conf index 9f2635a..0129b77 100644 --- a/default.conf +++ b/default.conf @@ -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; } -} \ No newline at end of file + + # 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; + } +} diff --git a/vendor/clouds3d.js b/vendor/clouds3d.js index 7506955..9b4c28c 100644 --- a/vendor/clouds3d.js +++ b/vendor/clouds3d.js @@ -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; }); }