From d8c8325faf4111391a7502efb80850381d86155b Mon Sep 17 00:00:00 2001 From: Dylan Jenkins Date: Thu, 17 Sep 2026 07:59:07 +0000 Subject: [PATCH 1/3] fix(deck.gl-geotiff): raise COG open failures through onError COGLayer and MultiCOGLayer open their GeoTIFFs from updateState without awaiting the promise, so a header fetch or CRS resolution that fails rejects with nothing attached: the browser logs an unhandled rejection, onError never fires and the layer stays empty. Both opens now route a rejection through Layer.raiseError, which calls props.onError and falls back to the Deck-level handler, the path deck.gl uses for a failed async data load. Closes #205 Co-Authored-By: Claude Fable 5.1 --- packages/deck.gl-geotiff/src/cog-layer.ts | 4 +++- .../deck.gl-geotiff/src/multi-cog-layer.ts | 6 ++++- .../deck.gl-geotiff/tests/cog-layer.test.ts | 22 +++++++++++++++++ .../tests/multi-cog-layer.test.ts | 24 +++++++++++++++++++ 4 files changed, 54 insertions(+), 2 deletions(-) diff --git a/packages/deck.gl-geotiff/src/cog-layer.ts b/packages/deck.gl-geotiff/src/cog-layer.ts index 92c268d1..3c085113 100644 --- a/packages/deck.gl-geotiff/src/cog-layer.ts +++ b/packages/deck.gl-geotiff/src/cog-layer.ts @@ -202,7 +202,9 @@ export class COGLayer< // Clear stale state so renderLayers returns null until the new GeoTIFF is // ready this.clearState(); - this._parseGeoTIFF(); + this._parseGeoTIFF().catch((error: Error) => + this.raiseError(error, "loading GeoTIFF"), + ); } } diff --git a/packages/deck.gl-geotiff/src/multi-cog-layer.ts b/packages/deck.gl-geotiff/src/multi-cog-layer.ts index 7b50a574..3e184eed 100644 --- a/packages/deck.gl-geotiff/src/multi-cog-layer.ts +++ b/packages/deck.gl-geotiff/src/multi-cog-layer.ts @@ -384,7 +384,11 @@ export class MultiCOGLayer extends RasterTileLayer< sources: null, multiDescriptor: null, }); - this._parseAllSources(); + // The open runs outside any tile request, so its rejection has no tile + // error path. raiseError reaches props.onError, then Deck's handler. + this._parseAllSources().catch((error: Error) => + this.raiseError(error, "loading COG sources"), + ); } } diff --git a/packages/deck.gl-geotiff/tests/cog-layer.test.ts b/packages/deck.gl-geotiff/tests/cog-layer.test.ts index 2b951866..8f5db54c 100644 --- a/packages/deck.gl-geotiff/tests/cog-layer.test.ts +++ b/packages/deck.gl-geotiff/tests/cog-layer.test.ts @@ -77,3 +77,25 @@ describe("COGLayer._onTileUnloadCallback", () => { expect(unloadCallback(layer)).toBe(onTileUnload); }); }); + +describe("COGLayer.updateState", () => { + it("raises a GeoTIFF open failure through onError", async () => { + const onError = vi.fn((_error: Error) => true); + const layer = new COGLayer({ + id: "cog", + geotiff: "https://example.com/x.tif", + onError, + } as never); + vi.spyOn(layer, "clearState").mockImplementation(() => {}); + vi.spyOn(layer, "_parseGeoTIFF").mockRejectedValue(new Error("boom")); + + layer.updateState({ + props: layer.props, + oldProps: layer.props, + changeFlags: { dataChanged: true }, + } as never); + + await vi.waitFor(() => expect(onError).toHaveBeenCalledOnce()); + expect(onError.mock.calls[0]?.[0]?.message).toBe("loading GeoTIFF: boom"); + }); +}); diff --git a/packages/deck.gl-geotiff/tests/multi-cog-layer.test.ts b/packages/deck.gl-geotiff/tests/multi-cog-layer.test.ts index bc7317ee..39702b01 100644 --- a/packages/deck.gl-geotiff/tests/multi-cog-layer.test.ts +++ b/packages/deck.gl-geotiff/tests/multi-cog-layer.test.ts @@ -54,3 +54,27 @@ describe("MultiCOGLayer._onTileUnloadCallback", () => { expect(() => cb?.({ content: null })).not.toThrow(); }); }); + +describe("MultiCOGLayer.updateState", () => { + it("raises a COG open failure through onError", async () => { + const onError = vi.fn((_error: Error) => true); + const layer = new MultiCOGLayer({ + id: "multi", + sources: { a: { url: "https://example.com/a.tif" } }, + onError, + } as never); + vi.spyOn(layer, "setState").mockImplementation(() => {}); + vi.spyOn(layer, "_parseAllSources").mockRejectedValue(new Error("boom")); + + layer.updateState({ + props: layer.props, + oldProps: layer.props, + changeFlags: { dataChanged: true }, + } as never); + + await vi.waitFor(() => expect(onError).toHaveBeenCalledOnce()); + const error = onError.mock.calls[0]?.[0]; + expect(error?.message).toBe("loading COG sources: boom"); + expect(error?.cause).toBeInstanceOf(Error); + }); +}); From 80b5fda8cec52c724058dbb78ce6206c120ed4ea Mon Sep 17 00:00:00 2001 From: Dylan Jenkins Date: Thu, 17 Sep 2026 18:29:51 +1000 Subject: [PATCH 2/3] Update multi-cog-layer.ts --- packages/deck.gl-geotiff/src/multi-cog-layer.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/deck.gl-geotiff/src/multi-cog-layer.ts b/packages/deck.gl-geotiff/src/multi-cog-layer.ts index 3e184eed..ef0697df 100644 --- a/packages/deck.gl-geotiff/src/multi-cog-layer.ts +++ b/packages/deck.gl-geotiff/src/multi-cog-layer.ts @@ -384,8 +384,6 @@ export class MultiCOGLayer extends RasterTileLayer< sources: null, multiDescriptor: null, }); - // The open runs outside any tile request, so its rejection has no tile - // error path. raiseError reaches props.onError, then Deck's handler. this._parseAllSources().catch((error: Error) => this.raiseError(error, "loading COG sources"), ); From 914bbad5b3cadb368f9f9cc463dca39fd08ddbaa Mon Sep 17 00:00:00 2001 From: Kyle Barron Date: Mon, 21 Sep 2026 16:18:49 -0400 Subject: [PATCH 3/3] catch CRS parsing errors --- packages/deck.gl-geotiff/src/cog-layer.ts | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/packages/deck.gl-geotiff/src/cog-layer.ts b/packages/deck.gl-geotiff/src/cog-layer.ts index 3c085113..f58d7c15 100644 --- a/packages/deck.gl-geotiff/src/cog-layer.ts +++ b/packages/deck.gl-geotiff/src/cog-layer.ts @@ -221,11 +221,17 @@ export class COGLayer< const signal = this.state.abortController?.signal; let geotiff: GeoTIFF; + let sourceProjection: ProjectionDefinition; try { geotiff = await fetchGeoTIFF(this.props.geotiff, { concurrencyLimiter: this.props.concurrencyLimiter, signal, }); + const crs = geotiff.crs; + sourceProjection = + typeof crs === "number" + ? await this.props.epsgResolver!(crs) + : parseWkt(crs); } catch (err) { // Layer removed mid-open (finalizeState aborted the signal); drop it. if (signal?.aborted) { @@ -233,11 +239,6 @@ export class COGLayer< } throw err; } - const crs = geotiff.crs; - const sourceProjection = - typeof crs === "number" - ? await this.props.epsgResolver!(crs) - : parseWkt(crs); if (signal?.aborted) { return;