From 68e1b634218c54c81faf52f6a8b9e5155540a530 Mon Sep 17 00:00:00 2001 From: Sirius DevOps Date: Sat, 29 Aug 2026 11:42:03 -0400 Subject: [PATCH] fix(titiler): drop @1x suffix + append SAS token top-level MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit TiTiler /tiles/{z}/{x}/{y} takes a strict int for y; the @1x scale-suffix template 422s on every SAR tile request (int_parsing on '21@1x'). sign_cog_url wrapped the Planetary Computer SAS token under a single token= param, which Azure rejects (403→409); the token is a pre-encoded query string and must be appended top-level (st=…&se=…&sig=…). Proven live: correct-form URL renders a 200 image/png 256px tile. --- app/live_layers.py | 12 +++++++++--- tests/test_live_layers.py | 19 ++++++++++--------- 2 files changed, 19 insertions(+), 12 deletions(-) diff --git a/app/live_layers.py b/app/live_layers.py index b6e80d1..eebf6c1 100644 --- a/app/live_layers.py +++ b/app/live_layers.py @@ -50,7 +50,7 @@ PC_SAS_TOKEN = "https://planetarycomputer.microsoft.com/api/sas/v1/token/sentine # Self-hosted TiTiler on the Pi, exposed same-origin through the osint.rpi.local # nginx vhost. Relative template — Leaflet resolves it against the page origin, # so the browser never touches a raw loopback port or titiler.xyz. -TITILER_COG_TILES = f"{TITILER_PUBLIC_BASE}/cog/tiles/WebMercatorQuad/{{z}}/{{x}}/{{y}}@1x" +TITILER_COG_TILES = f"{TITILER_PUBLIC_BASE}/cog/tiles/WebMercatorQuad/{{z}}/{{x}}/{{y}}" SENTINEL1_TTL = 20 * 60 # 15–30 min quota-friendly window SENTINEL1_ATTRIBUTION = "Copernicus Sentinel-1 / Microsoft Planetary Computer" @@ -1194,9 +1194,15 @@ async def _pc_call(coro: Awaitable[Any]) -> Any: def sign_cog_url(href: str, token: str) -> str: - """Append a SAS token to a PC blob URL (respect existing query string).""" + """Append a SAS token to a PC blob URL (respect existing query string). + + PC's SAS endpoint returns the token as an already-percent-encoded query + string (``st=…&se=…&sp=rl&…&sig=…``). Azure only honours those parameters + when they sit top-level on the blob URL — wrapping them under a single + ``token=`` param yields 403/409, so we append the token verbatim. + """ sep = "&" if "?" in href else "?" - return f"{href}{sep}token={quote(token, safe='')}" + return f"{href}{sep}{token}" def sentinel1_tile_url(signed_cog: str) -> str: diff --git a/tests/test_live_layers.py b/tests/test_live_layers.py index ebd4340..69f8164 100644 --- a/tests/test_live_layers.py +++ b/tests/test_live_layers.py @@ -661,18 +661,19 @@ def test_planespotters_headers_add_contact_when_ua_is_generic(monkeypatch): # ── Sentinel-1 SAR (Planetary Computer STAC → signed COG template) ──────── def test_sign_cog_url_appends_token(): - assert sign_cog_url("https://blob.example/x.tif", "tok=abc") == \ - "https://blob.example/x.tif?token=tok%3Dabc" + # PC returns the token pre-encoded as a query string; append verbatim. + assert sign_cog_url("https://blob.example/x.tif", "st=s&se=e&sig=x%3D") == \ + "https://blob.example/x.tif?st=s&se=e&sig=x%3D" # Existing query string → append with & - assert sign_cog_url("https://blob.example/x.tif?st=1", "tok") == \ - "https://blob.example/x.tif?st=1&token=tok" + assert sign_cog_url("https://blob.example/x.tif?foo=1", "st=s&sig=x") == \ + "https://blob.example/x.tif?foo=1&st=s&sig=x" def test_sentinel1_tile_url_contains_titiler_rescale_and_cfastie(): signed = "https://blob.example/x.tif?token=secret" url = sentinel1_tile_url(signed) assert url.startswith(TITILER_COG_TILES + "?") - assert "WebMercatorQuad/{z}/{x}/{y}@1x" in url + assert "WebMercatorQuad/{z}/{x}/{y}?" in url assert "url=https%3A%2F%2Fblob.example%2Fx.tif%3Ftoken%3Dsecret" in url assert "rescale=0%2C500" in url assert "colormap_name=cfastie" in url @@ -723,12 +724,12 @@ def test_fetch_sentinel1_vv_signed_tile_url(monkeypatch): assert out["opacity"] == 0.8 assert out["itemId"].startswith("S1A") assert out["attribution"] == SENTINEL1_ATTRIBUTION - assert "WebMercatorQuad/{z}/{x}/{y}@1x" in out["tileUrl"] + assert "WebMercatorQuad/{z}/{x}/{y}?" in out["tileUrl"] assert "rescale=0%2C500" in out["tileUrl"] assert "colormap_name=cfastie" in out["tileUrl"] - # SAS token "sig=abc123" is signed as ?token=sig%3Dabc123, then the whole - # COG URL is percent-encoded again as a query param (=> sig%253Dabc123). - assert "sig%253Dabc123" in out["tileUrl"] + # SAS token "sig=abc123" is appended top-level, then the whole COG URL is + # percent-encoded again as a query param (=> sig%3Dabc123). + assert "sig%3Dabc123" in out["tileUrl"] # STAC search payload shape post_url, post_json = calls[0][1], calls[0][2] assert post_url.endswith("/api/stac/v1/search")