Address review feedback

- _server.py: gate the Content-Range/X-GeoLibre-Content-Range headers on a
  newly parsed range rather than on the presence of a Range header, so a
  non-bytes unit (e.g. "items=0-1") no longer emits a spec-invalid
  Content-Range alongside a full-file 200.
- _server.py: send Access-Control-Expose-Headers for
  X-GeoLibre-Content-Range, without which a cross-origin fetch cannot read it
  and the app leaves the response at 200 instead of rebuilding the 206.
- _frontend.js: require an empty port on the Colab proxy hostname, so a
  look-alike host on a foreign network port does not enter the rewrite path.
- _frontend.js: extract the transport gate into an exported
  canProxyLocalFiles(base, port) and cover it in tests, so a future
  proxyBase/resolveBase refactor cannot silently stop the rewrite.
- geolibre.py: resolve the local raster path once and hand the resolved path
  to both register_raster_tiles and rasterio.open — GDAL does not expand "~",
  so a "~/dem.tif" source silently lost its computed bounds.
- kernel-proxy-range.ts: export KERNEL_CONTENT_RANGE_HEADER and cross-reference
  the three mirrored literals from _frontend.js and _server.py; a new test
  asserts both Python sources against the exported constants.
- python-api.md: fix the duplicated "JupyterHub when" clause, matching the
  wording in docs/python.md.
- tests: cover the malformed-range 200, the exposed range header, and both the
  populated and None bounds paths of add_cog's Colab branch.
This commit is contained in:
giswqs committed 2026-08-25 12:49:43 -04:00
1 parent 45599eff6a
commit 8af3e91c7d
8 files changed
+226 -11

No files matched your search

@@ -1,8 +1,15 @@
// These three strings are the wire contract of the Colab range bridge, and each
// is spelled out again on the Python side — the marker in
// `python/src/geolibre/_frontend.js`, the query parameter and the header in
// `python/src/geolibre/_server.py`. Neither can import from this module, so
// `tests/python-widget-frontend.test.ts` asserts those sources against the
// constants here; keep all three in step.
/** Query marker added by the Python widget to local raster URLs in Colab. */
export const KERNEL_RANGE_PROXY_MARKER = "__geolibre_range_proxy";
/** Query parameter carrying a Range header through proxies that remove it. */
export const KERNEL_RANGE_QUERY = "__geolibre_range";
const KERNEL_CONTENT_RANGE_HEADER = "X-GeoLibre-Content-Range";
/** Response header carrying the served span past a proxy that flattens 206. */
export const KERNEL_CONTENT_RANGE_HEADER = "X-GeoLibre-Content-Range";
/**
* Move a byte-range header into the URL for a marked kernel-local request.
+18 -2
View File
@@ -66,8 +66,11 @@ function proxiedLocalFileUrl(raw, base, port) {
// project synced back by an older view can therefore carry a valid token on
// that view's now-foreign origin. Recognize the same-port proxy hostname and
// rebase it onto the current view's proxy URL below.
// The proxy origin carries no explicit port; requiring an empty one keeps a
// look-alike host on some other network port out of the rewrite path.
const colabProxy =
parsed.protocol === "https:" &&
parsed.port === "" &&
parsed.hostname.endsWith(`-${port}-colab.googleusercontent.com`);
if (
((!loopback || parsed.port !== String(port)) && !colabProxy) ||
@@ -81,6 +84,9 @@ function proxiedLocalFileUrl(raw, base, port) {
// Mark these URLs so the embedded app can carry each requested byte range in
// the query string instead; the token-protected Python route translates it
// back into a normal 206 response. Other Jupyter proxies keep using headers.
// The marker mirrors KERNEL_RANGE_PROXY_MARKER in
// apps/geolibre-desktop/src/lib/kernel-proxy-range.ts, which reads it back;
// tests/python-widget-frontend.test.ts fails if the two drift.
if (
rewritten.pathname.startsWith("/_geolibre_local/") &&
rewritten.hostname.endsWith(`-${port}-colab.googleusercontent.com`)
@@ -92,6 +98,17 @@ function proxiedLocalFileUrl(raw, base, port) {
return rewritten.href.replace(/%7B([zxy])%7D/gi, "{$1}");
}
// Whether the browser can reach the kernel's tokenized local-file routes by
// rebasing them onto `base`. True in Colab, where the iframe itself is loaded
// through the per-port proxy, and wherever the app is already served by
// jupyter-server-proxy on that same kernel port. Under the Jupyter Server
// extension the app comes from a different route than the kernel's port, so
// the loopback URL cannot be rewritten and is left alone.
export function canProxyLocalFiles(base, port) {
if (!port) return false;
return Boolean(colabKernel()) || base === proxyBase(port);
}
export function rewriteProxiedLocalFileUrls(project, base, port) {
if (!project || typeof project !== "object" || !Array.isArray(project.layers) || !port) {
return project;
@@ -271,8 +288,7 @@ async function render({ model, el }) {
const pushProject = () => {
if (!ready) return;
const port = model.get("_app_port");
const canProxyLocalFiles = colabKernel() || base === proxyBase(port);
const project = canProxyLocalFiles
const project = canProxyLocalFiles(base, port)
? rewriteProxiedLocalFileUrls(model.get("project"), base, port)
: model.get("project");
post({
+17 -2
View File
@@ -89,10 +89,20 @@ class _QuietHandler(SimpleHTTPRequestHandler):
start, end, status = 0, size - 1, 200
range_header = self.headers.get("Range")
query_range = False
# Set only once a range actually parses. `status` cannot stand in
# for it: the query transport deliberately keeps a matched range at
# 200, and an unparsable unit (e.g. "items=0-1") leaves both the
# status and the full-file span untouched.
range_matched = False
# Google Colab's port proxy strips Range request headers. The
# embedded app moves the value into this query parameter only for a
# marked, tokenized local-file URL, preserving exact COG partial
# reads without downloading the full raster through the proxy.
# reads without downloading the full raster through the proxy. The
# parameter and the response header below mirror KERNEL_RANGE_QUERY
# and KERNEL_CONTENT_RANGE_HEADER in
# apps/geolibre-desktop/src/lib/kernel-proxy-range.ts, which writes
# and reads them; tests/python-widget-frontend.test.ts asserts this
# file against those constants.
if not range_header:
range_header = parse_qs(urlsplit(self.path).query).get("__geolibre_range", [None])[
0
@@ -106,6 +116,7 @@ class _QuietHandler(SimpleHTTPRequestHandler):
self.end_headers()
return
start, end = parsed
range_matched = True
# Colab's proxy can normalize an upstream 206 into a full-file
# 200 response. For the query transport, send the requested
# slice as an ordinary 200 and let the embedded app reconstruct
@@ -120,9 +131,13 @@ class _QuietHandler(SimpleHTTPRequestHandler):
# working if the app is served from a different origin (the file
# server is loopback-only and serves only registered tokens).
self.send_header("Access-Control-Allow-Origin", "*")
if range_header:
if range_matched:
if query_range:
self.send_header("X-GeoLibre-Content-Range", f"bytes {start}-{end}/{size}")
# A non-safelisted response header is hidden from fetch()
# unless it is exposed, and the app needs to read it to
# rebuild the 206 the proxy flattened.
self.send_header("Access-Control-Expose-Headers", "X-GeoLibre-Content-Range")
else:
self.send_header("Content-Range", f"bytes {start}-{end}/{size}")
self.end_headers()
+5 -2
View File
@@ -1908,8 +1908,11 @@ class Map(anywidget.AnyWidget):
if self._running_on_colab() and not (
isinstance(url, str) and url.startswith(("http://", "https://"))
):
# Resolve once so rasterio sees the same file the tile route
# registered; GDAL does not expand "~" on its own.
local_path = pathlib.Path(url).expanduser().resolve()
tile_url = register_raster_tiles(
url,
local_path,
bands=bands,
colormap=colormap,
rescale=rescale,
@@ -1918,7 +1921,7 @@ class Map(anywidget.AnyWidget):
import rasterio
from rasterio.warp import transform_bounds
with rasterio.open(url) as dataset:
with rasterio.open(local_path) as dataset:
bounds = list(
transform_bounds(
dataset.crs,
+52
View File
@@ -717,6 +717,58 @@ def test_add_raster_xarray_uses_xyz_tiles_on_colab(monkeypatch, m):
}
def test_add_cog_on_colab_computes_bounds_from_the_local_raster(monkeypatch, m, tmp_path):
"""The Colab tile branch reads the raster's own extent, reprojected to WGS84."""
rasterio = pytest.importorskip("rasterio")
np = pytest.importorskip("numpy")
from rasterio.transform import from_bounds
raster = tmp_path / "dem.tif"
with rasterio.open(
raster,
"w",
driver="GTiff",
width=4,
height=4,
count=1,
dtype="float32",
crs="EPSG:4326",
transform=from_bounds(-120, 30, -100, 45, 4, 4),
) as dst:
dst.write(np.zeros((1, 4, 4), dtype="float32"))
monkeypatch.setattr(Map, "_running_on_colab", staticmethod(lambda: True))
monkeypatch.setattr(
gmod,
"register_raster_tiles",
lambda path, **_options: "http://127.0.0.1:1234/_geolibre_tiles/t/{z}/{x}/{y}.png",
)
# "~" is what GDAL will not expand on its own, so hand add_cog that form.
monkeypatch.setenv("HOME", str(tmp_path))
monkeypatch.setenv("USERPROFILE", str(tmp_path))
m.add_cog("~/dem.tif", name="DEM")
bounds = _last_layer(m)["source"]["bounds"]
assert bounds == pytest.approx([-120, 30, -100, 45], abs=1e-6)
def test_add_cog_on_colab_tolerates_an_unreadable_raster(monkeypatch, m, tmp_path):
"""A file rasterio cannot open still yields a layer, just without bounds."""
raster = tmp_path / "not-a.tif"
raster.write_bytes(b"fake geotiff")
monkeypatch.setattr(Map, "_running_on_colab", staticmethod(lambda: True))
monkeypatch.setattr(
gmod,
"register_raster_tiles",
lambda path, **_options: "http://127.0.0.1:1234/_geolibre_tiles/t/{z}/{x}/{y}.png",
)
m.add_cog(raster, name="DEM")
assert _last_layer(m)["source"].get("bounds") is None
def test_add_raster_accepts_deprecated_url_keyword(m):
"""The pre-rename `url=` keyword still works, with a DeprecationWarning."""
with pytest.warns(DeprecationWarning, match="add_raster\\(url=...\\) is deprecated"):
+17
View File
@@ -106,6 +106,23 @@ def test_open_ended_range(served):
assert body == payload[10200:]
def test_non_bytes_range_unit_serves_the_whole_file(served):
"""An unparsable range unit falls back to a plain 200, with no Content-Range."""
status, headers, body = _get(served[0], {"Range": "items=0-99"})
assert status == 200
assert body == served[1]
# Content-Range belongs on 206/416 only; emitting it here would contradict
# the full-file body.
assert "Content-Range" not in headers
assert "X-GeoLibre-Content-Range" not in headers
def test_query_range_exposes_its_header_cross_origin(served):
"""The custom header is unreadable from fetch() unless it is exposed."""
_status, headers, _body = _get(f"{served[0]}?__geolibre_range=bytes%3D0-9")
assert headers["Access-Control-Expose-Headers"] == "X-GeoLibre-Content-Range"
def test_unsatisfiable_range_returns_416(served):
url, payload = served
with pytest.raises(urllib.error.HTTPError) as excinfo:
+2 -2
View File
@@ -136,8 +136,8 @@ durable.
`add_raster` / `add_cog` accept a **local** GeoTIFF path on the kernel host: the
bundled localhost server serves it so the app can read it. This works directly
in local Jupyter and VS Code. Colab uses kernel-rendered PNG XYZ tiles instead
of browser COG reads. JupyterHub works through the kernel port proxy when
JupyterHub when `jupyter-server-proxy` is available. A deployment that can only
of browser COG reads. JupyterHub can route the COG through the kernel port
when `jupyter-server-proxy` is available. A deployment that can only
serve the static app extension cannot expose kernel files, so use a hosted URL.
The served URL is session-scoped, so a saved project or exported HTML will not
restore a local raster later. **For anything durable or shareable, use a hosted
+107 -2
View File
@@ -1,7 +1,20 @@
import assert from "node:assert/strict";
import { describe, it } from "node:test";
import { readFileSync } from "node:fs";
import { fileURLToPath } from "node:url";
import { afterEach, describe, it } from "node:test";
import { rewriteProxiedLocalFileUrls } from "../python/src/geolibre/_frontend.js";
import {
KERNEL_CONTENT_RANGE_HEADER,
KERNEL_RANGE_PROXY_MARKER,
KERNEL_RANGE_QUERY,
} from "../apps/geolibre-desktop/src/lib/kernel-proxy-range";
import {
canProxyLocalFiles,
rewriteProxiedLocalFileUrls,
} from "../python/src/geolibre/_frontend.js";
const readSource = (relative: string) =>
readFileSync(fileURLToPath(new URL(`../${relative}`, import.meta.url)), "utf8");
describe("rewriteProxiedLocalFileUrls", () => {
it("routes registered raster files through the Colab proxy without mutating the project", () => {
@@ -110,4 +123,96 @@ describe("rewriteProxiedLocalFileUrls", () => {
project,
);
});
it("leaves a look-alike Colab host on a foreign port alone", () => {
const project = {
layers: [
{
source: {
url:
"https://old-view-41123-colab.googleusercontent.com:9999/" +
"_geolibre_local/token/a.tif",
},
},
],
};
assert.equal(
rewriteProxiedLocalFileUrls(
project,
"https://session-41123-colab.googleusercontent.com/",
41123,
),
project,
);
});
});
describe("canProxyLocalFiles", () => {
const setWindow = (extras: Record<string, unknown> = {}) => {
(globalThis as Record<string, unknown>).window = {
location: { href: "https://hub.example/user/alice/lab" },
...extras,
};
(globalThis as Record<string, unknown>).document = {
getElementById: () => ({ textContent: JSON.stringify({ baseUrl: "/user/alice/" }) }),
};
};
afterEach(() => {
delete (globalThis as Record<string, unknown>).window;
delete (globalThis as Record<string, unknown>).document;
});
it("rewrites when the app is itself served by jupyter-server-proxy", () => {
setWindow();
assert.equal(canProxyLocalFiles("https://hub.example/user/alice/proxy/41123/", 41123), true);
});
it("leaves loopback URLs alone under the Jupyter Server extension", () => {
setWindow();
assert.equal(canProxyLocalFiles("https://hub.example/user/alice/geolibre/app/", 41123), false);
// The proxy route for another port is not this widget's transport either.
assert.equal(canProxyLocalFiles("https://hub.example/user/alice/proxy/9999/", 41123), false);
});
it("rewrites on any base once a Colab kernel is present", () => {
setWindow({ google: { colab: { kernel: {} } } });
assert.equal(
canProxyLocalFiles("https://session-41123-colab.googleusercontent.com/", 41123),
true,
);
});
it("declines before the app port is known", () => {
setWindow({ google: { colab: { kernel: {} } } });
assert.equal(
canProxyLocalFiles("https://session-41123-colab.googleusercontent.com/", 0),
false,
);
});
});
describe("the Colab range-bridge wire contract", () => {
it("is spelled the same way on the Python side", () => {
const frontend = readSource("python/src/geolibre/_frontend.js");
const server = readSource("python/src/geolibre/_server.py");
assert.ok(
frontend.includes(`searchParams.set("${KERNEL_RANGE_PROXY_MARKER}", "1")`),
"the widget must mark proxied local-file URLs with KERNEL_RANGE_PROXY_MARKER",
);
assert.ok(
server.includes(`.get("${KERNEL_RANGE_QUERY}", [None])`),
"the local-file route must read the range from KERNEL_RANGE_QUERY",
);
assert.ok(
server.includes(`self.send_header("${KERNEL_CONTENT_RANGE_HEADER}"`),
"the local-file route must report its span in KERNEL_CONTENT_RANGE_HEADER",
);
assert.ok(
server.includes(`"Access-Control-Expose-Headers", "${KERNEL_CONTENT_RANGE_HEADER}"`),
"the custom range header must be exposed to cross-origin fetches",
);
});
});