Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,11 @@
"format:check": "oxfmt --check package.json .vscode .eslintrc.cjs .github *.md .devcontainer .oxfmtrc.json",
"check-deps": "tsx scripts/check-deps.ts"
},
"lint-staged": {
"**/*.{cjs,ts}": [
"oxfmt"
]
},
"devDependencies": {
"@types/node": "^22.14.1",
"@typescript-eslint/eslint-plugin": "^7.2.0",
Expand Down
10 changes: 7 additions & 3 deletions packages/hub/src/utils/WebBlob.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,14 @@ describe("WebBlob", () => {
let contentType: string;

beforeAll(async () => {
const response = await fetch(resourceUrl, { method: "HEAD" });
size = Number(response.headers.get("content-length"));
// Compute the reference size from the response body itself; in browsers
// `Content-Length` is not reliably exposed when the response is gzipped
// on the fly by CloudFront.
const response = await fetch(resourceUrl);
const blob = await response.blob();
size = blob.size;
fullText = await blob.text();
contentType = response.headers.get("content-type") || "";
fullText = await (await fetch(resourceUrl)).text();
});

it("should create a WebBlob with a slice on the entire resource", async () => {
Expand Down
57 changes: 44 additions & 13 deletions packages/hub/src/utils/WebBlob.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,24 +22,55 @@ interface WebBlobCreateOptions {
export class WebBlob extends Blob {
static async create(url: URL, opts?: WebBlobCreateOptions): Promise<Blob> {
const customFetch = opts?.fetch ?? fetch;
const response = await customFetch(url, {
method: "HEAD",
...(opts?.accessToken && {
headers: {
Authorization: `Bearer ${opts.accessToken}`,
},
}),

// Probe with `Range: bytes=0-0` rather than `HEAD` to learn the file size
// and confirm range support in a single round trip.
//
// In browsers, when CloudFront gzips a response on the fly (typical for
// small text/JSON files behind `/api/resolve-cache/...`), the cached
// response loses both `Content-Length` and `Accept-Ranges`. Subsequent
// HEAD requests served from that cache inherit the missing headers, so
// the lib could not tell either the file size or whether ranges were
// supported, and silently fell back to buffering the whole blob in RAM.
//
// Range responses are never content-encoded, so `Content-Range`
// (carrying the total size) and the strong `ETag` always survive,
// regardless of the cached encoding state.
const probe = await customFetch(url, {
headers: {
Range: "bytes=0-0",
...(opts?.accessToken && { Authorization: `Bearer ${opts.accessToken}` }),
},
});

const size = Number(response.headers.get("content-length"));
const contentType = response.headers.get("content-type") || "";
const supportRange = response.headers.get("accept-ranges") === "bytes";
if (!probe.ok) {
throw await createApiError(probe);
}
Comment thread
coyotte508 marked this conversation as resolved.

if (!supportRange || size < (opts?.cacheBelow ?? 1_000_000)) {
return await (await customFetch(url)).blob();
const contentType = probe.headers.get("content-type") || "";

// 206 → server honored the range request; total size is in `Content-Range`.
if (probe.status === 206) {
const totalSize = Number(probe.headers.get("content-range")?.split("/").pop());
await probe.body?.cancel();

if (Number.isFinite(totalSize) && totalSize >= (opts?.cacheBelow ?? 1_000_000)) {
return new WebBlob(url, 0, totalSize, contentType, true, customFetch, opts?.accessToken);
}

// Small file (or unknown total) → buffer it in RAM.
const full = await customFetch(url, {
...(opts?.accessToken && { headers: { Authorization: `Bearer ${opts.accessToken}` } }),
});
if (!full.ok) {
throw await createApiError(full);
}
return full.blob();
}

return new WebBlob(url, 0, size, contentType, true, customFetch, opts?.accessToken);
// 200 → server ignored `Range`; we've already started downloading the
// full body, so just consume it.
return probe.blob();
}

private url: URL;
Expand Down
Loading