Skip to content

Commit 64fef04

Browse files
ryan-williamsclaude
andcommitted
Fix Zarr loading against S3 (no-list IAM)
Two issues blocked loading from `s3://openathena/electrai/zarr/`: 1. zarrita's `open()` probes for v3 `zarr.json` first; on miss it tries v2. Switched to `open.v2()` directly to skip the v3 probe. 2. S3 returns 403 (not 404) for missing keys when the IAM lacks `s3:ListBucket` — so probes for nonexistent files (e.g., `0/.zattrs`) throw instead of falling through. Wrapped FetchStore's fetch to remap 403 -> 404 so zarrita's response handler treats them as missing keys. Verified end-to-end against `s3://openathena/electrai/zarr/mp-1775579-input.zarr/`: metadata + level 0 voxels load, marching cubes renders, iso slider tracks quantile table from .zattrs. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent b80a24d commit 64fef04

1 file changed

Lines changed: 18 additions & 6 deletions

File tree

pkgs/core/src/storage/zarr-volume.ts

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -30,15 +30,27 @@ export interface OpenedZarrVolume {
3030
levelShapes: [number, number, number][]
3131
}
3232

33+
/**
34+
* S3 returns 403 (not 404) for missing keys when the IAM lacks `s3:ListBucket`.
35+
* zarrita's FetchStore expects 404 to signal "missing key" — so we wrap fetch
36+
* to remap 403 responses to a synthetic 404. Harmless: real auth failures on
37+
* private objects still surface as 404 ("not found"), which zarrita interprets
38+
* correctly, and the response body for the original 403 isn't useful anyway.
39+
*/
40+
function fetchWithS3MissingKeyShim(request: Request): Promise<Response> {
41+
return fetch(request).then(r => r.status === 403 ? new Response(null, { status: 404 }) : r)
42+
}
43+
3344
/** Open a multi-resolution Zarr store and read its metadata + per-level shapes. */
3445
export async function openZarrVolume(url: string): Promise<OpenedZarrVolume> {
35-
const store = new zarr.FetchStore(url)
36-
const root = await zarr.open(store, { kind: 'group' })
46+
const store = new zarr.FetchStore(url, { fetch: fetchWithS3MissingKeyShim })
47+
// Use v2-specific opener to skip the v3 `zarr.json` probe.
48+
const root = await zarr.open.v2(store, { kind: 'group' })
3749
const meta = root.attrs as unknown as ZarrVolumeMetadata
3850
const levels = meta.multiscales[0].datasets.length
3951
const levelShapes: [number, number, number][] = []
4052
for (let i = 0; i < levels; i++) {
41-
const arr = await zarr.open(root.resolve(String(i)), { kind: 'array' })
53+
const arr = await zarr.open.v2(root.resolve(String(i)), { kind: 'array' })
4254
levelShapes.push(arr.shape as [number, number, number])
4355
}
4456
return { url, meta, levels, levelShapes }
@@ -49,9 +61,9 @@ export async function readZarrLevel(
4961
opened: OpenedZarrVolume,
5062
level: number,
5163
): Promise<{ data: Float32Array; dims: [number, number, number] }> {
52-
const store = new zarr.FetchStore(opened.url)
53-
const root = await zarr.open(store, { kind: 'group' })
54-
const arr = await zarr.open(root.resolve(String(level)), { kind: 'array' })
64+
const store = new zarr.FetchStore(opened.url, { fetch: fetchWithS3MissingKeyShim })
65+
const root = await zarr.open.v2(store, { kind: 'group' })
66+
const arr = await zarr.open.v2(root.resolve(String(level)), { kind: 'array' })
5567
const result = await zarr.get(arr)
5668
// zarrita returns C-ordered raw bytes; pymatgen->Zarr writes in C-order too.
5769
// VASP/marching-cubes expects F-order (flat[i + j*Nx + k*Nx*Ny] = data[i,j,k]).

0 commit comments

Comments
 (0)