Merge pull request 'fix(titiler): drop @1x suffix + append SAS token top-level' (#24) from fix/titiler-tile-url into master
All checks were successful
build-and-deploy / build-push-deploy (push) Successful in 3m12s

Reviewed-on: #24
This commit is contained in:
sirius 2026-08-29 12:13:33 -04:00
commit fc2c7c28c3
2 changed files with 19 additions and 12 deletions

View file

@ -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 # 1530 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:

View file

@ -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")