From c1014a5cf8b4fc0254203dea25b85f29bc24ad85 Mon Sep 17 00:00:00 2001 From: Brandon Liu Date: Fri, 27 Feb 2026 11:09:11 -0500 Subject: [PATCH] js: Allow passing credentials option to fetch [#397] (#644) * js: Allow passing credentials option to fetch [#397] * fix passing custom headers in case where remote archive is < 16 kB * clean up `any` usage --- js/src/index.ts | 21 ++++++++++++++------- js/test/adapter.test.ts | 2 +- js/test/utils.ts | 32 +++++++++++++++++++++++++++++++- js/test/v3.test.ts | 34 ++++++++++++++++++++++++++++++++++ 4 files changed, 80 insertions(+), 9 deletions(-) diff --git a/js/src/index.ts b/js/src/index.ts index 7a2121b..5631517 100644 --- a/js/src/index.ts +++ b/js/src/index.ts @@ -345,14 +345,20 @@ export class FetchSource implements Source { * This should be used instead of maplibre's [transformRequest](https://maplibre.org/maplibre-gl-js/docs/API/classes/Map/#example) for PMTiles archives. */ customHeaders: Headers; + credentials: "same-origin" | "include" | undefined; /** @hidden */ mustReload: boolean; /** @hidden */ chromeWindowsNoCache: boolean; - constructor(url: string, customHeaders: Headers = new Headers()) { + constructor( + url: string, + customHeaders: Headers = new Headers(), + credentials: "same-origin" | "include" | undefined = undefined + ) { this.url = url; this.customHeaders = customHeaders; + this.credentials = credentials; this.mustReload = false; let userAgent = ""; if ("navigator" in globalThis) { @@ -402,7 +408,7 @@ export class FetchSource implements Source { // * it requires CORS configuration becasue If-Match is not a CORs-safelisted header // CORs configuration should expose ETag. // if any etag mismatch is detected, we need to ignore the browser cache - let cache: string | undefined; + let cache: "no-store" | "reload" | undefined; if (this.mustReload) { cache = "reload"; } else if (this.chromeWindowsNoCache) { @@ -413,8 +419,8 @@ export class FetchSource implements Source { signal: signal, cache: cache, headers: requestHeaders, - //biome-ignore lint: "cache" is incompatible between cloudflare workers and browser - } as any); + credentials: this.credentials, + }); // handle edge case where the archive is < 16384 kb total. if (offset === 0 && resp.status === 416) { @@ -423,12 +429,13 @@ export class FetchSource implements Source { throw new Error("Missing content-length on 416 response"); } const actualLength = +contentRange.substr(8); + requestHeaders.set("range", `bytes=0-${actualLength - 1}`); resp = await fetch(this.url, { signal: signal, cache: "reload", - headers: { range: `bytes=0-${actualLength - 1}` }, - //biome-ignore lint: "cache" is incompatible between cloudflare workers and browser - } as any); + headers: requestHeaders, + credentials: this.credentials, + }); } // if it's a weak etag, it's not useful for us, so ignore it. diff --git a/js/test/adapter.test.ts b/js/test/adapter.test.ts index a6a75d9..5d6f61c 100644 --- a/js/test/adapter.test.ts +++ b/js/test/adapter.test.ts @@ -1,5 +1,5 @@ import assert from "node:assert"; -import { describe, mock, test } from "node:test"; +import { describe, test } from "node:test"; import { PMTiles, Protocol } from "../src"; import { mockServer } from "./utils"; diff --git a/js/test/utils.ts b/js/test/utils.ts index 5ab0e4d..d70d752 100644 --- a/js/test/utils.ts +++ b/js/test/utils.ts @@ -6,21 +6,27 @@ class MockServer { etag?: string; numRequests: number; lastCache?: string; + lastRequestHeaders: Headers | null; + lastCredentials?: string; reset() { this.numRequests = 0; this.etag = undefined; + this.lastRequestHeaders = null; } constructor() { this.numRequests = 0; this.etag = undefined; + this.lastRequestHeaders = null; const serverBuffer = fs.readFileSync("test/data/test_fixture_1.pmtiles"); const server = setupServer( http.get( "http://localhost:1337/example.pmtiles", ({ request, params }) => { this.lastCache = request.cache; + this.lastRequestHeaders = request.headers; + this.lastCredentials = request.credentials; this.numRequests++; const range = request.headers.get("range")?.substr(6).split("-"); if (!range) { @@ -35,7 +41,31 @@ class MockServer { headers: { etag: this.etag } as HeadersInit, }); } - ) + ), + http.get("http://localhost:1337/small.pmtiles", ({ request }) => { + this.lastRequestHeaders = request.headers; + this.lastCredentials = request.credentials; + this.numRequests++; + const range = request.headers.get("range")?.substr(6).split("-"); + if (!range) { + throw new Error("invalid range"); + } + const rangeEnd = +range[1]; + if (rangeEnd >= serverBuffer.length) { + return new HttpResponse(null, { + status: 416, + headers: { + "Content-Range": `bytes */${serverBuffer.length}`, + }, + }); + } + const rangeStart = +range[0]; + const body = serverBuffer.slice(rangeStart, rangeEnd + 1); + return new HttpResponse(body, { + status: 206, + headers: { etag: this.etag } as HeadersInit, + }); + }) ); server.listen({ onUnhandledRequest: "error" }); } diff --git a/js/test/v3.test.ts b/js/test/v3.test.ts index 7ad0c02..8329577 100644 --- a/js/test/v3.test.ts +++ b/js/test/v3.test.ts @@ -6,6 +6,7 @@ import { mockServer } from "./utils"; import { BufferPosition, Entry, + FetchSource, PMTiles, RangeResponse, SharedPromiseCache, @@ -407,3 +408,36 @@ describe("pmtiles v3", () => { }); }); }); + +describe("FetchSource", () => { + test("customHeaders are sent with requests", async () => { + mockServer.reset(); + const source = new FetchSource( + "http://localhost:1337/example.pmtiles", + new Headers({ "X-Custom-Header": "test-value" }), + "include" + ); + await source.getBytes(0, 100); + assert.strictEqual( + mockServer.lastRequestHeaders?.get("x-custom-header"), + "test-value" + ); + assert.strictEqual(mockServer.lastCredentials, "include"); + }); + + test("customHeaders are preserved on 416 retry", async () => { + mockServer.reset(); + const source = new FetchSource( + "http://localhost:1337/small.pmtiles", + new Headers({ "X-Custom-Header": "retry-value" }), + "include" + ); + await source.getBytes(0, 16384); + assert.strictEqual(mockServer.numRequests, 2); + assert.strictEqual( + mockServer.lastRequestHeaders?.get("x-custom-header"), + "retry-value", + "include" + ); + }); +});