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
This commit is contained in:
Brandon Liu
2026-02-27 11:09:11 -05:00
committed by GitHub
parent 9ff3871133
commit c1014a5cf8
4 changed files with 80 additions and 9 deletions

View File

@@ -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.

View File

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

View File

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

View File

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