Enable CORS + HTTP Range support for browser partial reads on cas-bridge.xethub.hf.co (Parquet row-group access)

### Feature request

## Summary
Browser-based data tools need Range requests to read Parquet efficiently (footer + selected row groups). Downloads from the Hub redirect to cas-bridge.xethub.hf.co (Xet bridge). The redirected host fails CORS preflight for Range/HEAD workflows, blocking partial reads. ([Hugging Face](https://huggingface.co/blog/migrating-the-hub-to-xet)). See example [HuggingFace dataset](https://huggingface.co/datasets/cornhundred/Xenium_V1_human_Pancreas_FFPE_outs_row_groups/tree/main/Xenium_V1_human_Pancreas_FFPE_outs_row_groups)

## Current behavior
Plain GET works via redirect.
Range workflows fail with: “Response to preflight request doesn’t pass access control check: It does not have HTTP ok status.”
This blocks parquet-wasm and DuckDB-Wasm style readers which rely on HEAD + Range or non-safelisted Range patterns. ([GitHub](https://github.com/duckdb/duckdb-wasm/issues/1852))
## Expected behavior
OPTIONS to the final redirected host returns 200/204 (no redirect) with appropriate CORS headers. Preflight responses must be “ok” status. ([GitHub](https://github.com/whatwg/fetch/issues/1588))
GET with Range returns 206 Partial Content and includes CORS headers, plus exposes Content-Range, Accept-Ranges, and Content-Length so browser JS can consume them. ([MDN WebDocument](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Content-Range))
## Proposed CORS headers (public, anonymous files)
For responses from cas-bridge.xethub.hf.co (and any sibling Xet bridge hosts):

### Preflight (OPTIONS)

Access-Control-Allow-Origin: *
Access-Control-Allow-Methods: GET, HEAD, OPTIONS
Access-Control-Allow-Headers: Range, Content-Type (or echo Access-Control-Request-Headers)
Access-Control-Max-Age: 86400 (optional, reduces preflight spam)
### Actual (GET/HEAD, including 206)

Access-Control-Allow-Origin: *
Access-Control-Expose-Headers: Content-Range, Accept-Ranges, Content-Length
Ensure Accept-Ranges: bytes and Content-Range are present for range responses. ([MDN WebDocument](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Accept-Ranges))
### Notes on credentials (optional)
If any endpoint requires credentials, wildcard * cannot be used and the server must echo Origin and add Vary: Origin. ([MDN WebDocument](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Access-Control-Allow-Origin))

## Impact
This unblocks efficient browser analytics and visualization on HF-hosted datasets using Parquet row groups, DuckDB-Wasm, parquet-wasm, and similar tooling. DuckDB-Wasm documentation explicitly notes that remote data access requires correct CORS on the hosting site. ([DuckDB](https://duckdb.org/docs/stable/clients/wasm/extensions.html))

High-quality references worth linking in the issue thread
Hugging Face: redirect to cas-bridge.xethub.hf.co shown in Xet migration blog ([Hugging Face](https://huggingface.co/blog/migrating-the-hub-to-xet))
Fetch/CORS: preflight must be “ok” status (200/204) ([GitHub](https://github.com/whatwg/fetch/issues/1588))
Fetch/CORS: redirect + preflight is a known sharp edge ([GitHub](https://github.com/whatwg/fetch/issues/204))
MDN CORS guide: Range safelist caveat ([MDN WebDocument](https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/CORS))
MDN Range header: single-range is safelisted, multi-range may preflight ([MDN WebDocument](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Range))
MDN Expose-Headers: non-safelisted headers must be exposed ([MDN WebDocument](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Access-Control-Expose-Headers))
DuckDB-Wasm: remote HTTPFS requires correct CORS ([DuckDB](https://duckdb.org/docs/stable/clients/wasm/extensions.html))
DuckDB-Wasm issue: HEAD blocked by CORS breaks the pipeline ([GitHub](https://github.com/duckdb/duckdb-wasm/issues/1852))
pdf.js historical issues about Accept-Ranges/Content-Range exposure ([GitHub](https://github.com/mozilla/pdf.js/issues/3150))
## Summary
Your request is standard: browser Parquet needs byte ranges.
Redirect to cas-bridge.xethub.hf.co makes CORS enforcement happen on the Xet bridge host. ([Hugging Face](https://huggingface.co/blog/migrating-the-hub-to-xet))
Fix requires: OPTIONS returns 200/204 with CORS headers, and 206 responses include CORS + exposed headers. ([GitHub](https://github.com/whatwg/fetch/issues/1588))
Similar failures exist across pdf.js and DuckDB-Wasm ecosystems. ([GitHub](https://github.com/duckdb/duckdb-wasm/issues/1852))



### Motivation

I would like to be able to read subsets of large Parquet files using range requests using the parquet_wasm library on the front end. This is being used as part of a spatial data visualization project https://github.com/broadinstitute/celldega

### Your contribution

I would be happy to provide code to make front-end range requests as an example.

Comments:
Cc @assafvayner maybe ? or @cfahlgren1 @severo if you've already encountered this ?

OK, reproduced with hyparquet on https://huggingface.co/spaces/hyperparam/hyperparam, see https://huggingface.co/spaces/hyperparam/hyperparam?url=https%3A%2F%2Fhuggingface.co%2Fdatasets%2Ffacebook%2Fresearch-plan-gen%2Fblob%2Frefs%2Fconvert%2Fparquet%2Farxiv%2Ftest%2F0000.parquet for example

Error message:

```
Cross-Origin Request Blocked: The Same Origin Policy disallows reading the remote resource at https://cas-bridge.xethub.hf.co/xet-bridge-us/695138b5329f4825326ac6c8/8e12ca920d225791200b59843ddb8e469b1c3c59f92cc2a9ffdec88b16ca00f6?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Content-Sha256=UNSIGNED-PAYLOAD&X-Amz-Credential=...&X-Amz-Date=20260109T124456Z&X-Amz-Expires=3600&X-Amz-Signature=...
```

Note that it works in https://hyparam.github.io/demos/hyparquet/?key=https%3A%2F%2Fhuggingface.co%2Fdatasets%2Ffacebook%2Fresearch-plan-gen%2Fresolve%2Frefs%252Fconvert%252Fparquet%2Farxiv%2Ftest%2F0000.parquet, which uses a more recent version of hyparquet.

So, my guess is:

- HEAD is forbidden in the S3 bucket (clients use HEAD to get the Parquet file size),
- a possible fix, on client side, is to use GET with a 0-byte length: https://github.com/hyparam/hyparquet/pull/137.

On HF side, we should allow HEAD on signed URLs

cc @coyotte508 too, just in case

Possibly, the solution is to add `HEAD` in `AllowedMethods` in the S3 bucket configuration, as here:

```json
[
    {
        "AllowedHeaders": [
            "*"
        ],
        "AllowedMethods": [
            "HEAD",
            "GET"
        ],
        "AllowedOrigins": [
            "*"
        ],
        "ExposeHeaders": [
            "Content-Range",
            "ETag",
            "x-amz-checksum-crc32"
        ]
    }
]
```

cc @assafvayner @rajatarya @Hugoch  for viz / fix xet-side

a very annoying "feature" with S3 is that presigned GET / HEAD urls aren't compatible with each other, eg a presigned GET can't do HEAD calls, which led to a host of issues and hacks on our side. We even escalated to AWS a few times, without success.

Note that Cloudfront is ok, we can use a presigend GET url for HEAD calls with cloudfront, just not with S3 directly.

Since the CAS bridge is not an actual S3 bucket, I would love for it to be able to respond to HEAD requests with a presigned GET url.

(note: maybe it's just a CORS issue and not a presigned URL issue 🤷 )

Thanks for reporting this @cornhundred - we (Xet team) will take on the changes to add appropriate CORS headers to our Bridge service to enable this use case.

@severo : Do the repro steps still work for you? I don't see any errors when I go to https://huggingface.co/spaces/hyperparam/hyperparam?url=https%3A%2F%2Fhuggingface.co%2Fdatasets%2Ffacebook%2Fresearch-plan-gen%2Fblob%2Frefs%2Fconvert%2Fparquet%2Farxiv%2Ftest%2F0000.parquet

Either @cornhundred or @severo can you give me repro steps so we can be sure we've got this fixed correctly?

And I've filed https://linear.app/xet/issue/XET-815/bridge-add-cors-headers-to-support-parquet-range-reads to track this issue on the Xet side. I'll keep updating this GH issue with progress, but this way we won't lose track of this.

It looks like the bug has been fixed indeed. The HEAD request returns 200 and the response is used by the JS client.

> It looks like the bug has been fixed indeed. The HEAD request returns 200 and the response is used by the JS client.

Well now I'm confused, because I'm pretty sure we didn't change/deploy anything on the Xet side related to this.

hmm, me too. I cannot reproduce the issue.

Here is a screenshot of the HEAD request, which was an error 3 days ago:

<img width="1839" height="1718" alt="Image" src="https://github.com/user-attachments/assets/a2e49839-8ca5-405b-a7a4-c26489e9b417" />

The response headers:

```
HTTP/1.1 200 OK
Content-Length: 4086406
Connection: keep-alive
Content-Disposition: inline; filename*=UTF-8''0000.parquet; filename="0000.parquet";
Cache-Control: public, max-age=31536000
ETag: "8e12ca920d225791200b59843ddb8e469b1c3c59f92cc2a9ffdec88b16ca00f6"
access-control-allow-origin: *
access-control-allow-headers: Content-Range, Content-Type, Content-Disposition, ETag
access-control-expose-headers: Accept-Ranges, Content-Range, Content-Type, Content-Disposition, ETag, X-Cache
Accept-Ranges: bytes
x-request-id: 01KEHD46KW533MGPAK7BXHY336
Date: Fri, 09 Jan 2026 12:53:56 GMT
X-Cache: Hit from cloudfront
Via: 1.1 07cb86faf6a141962da4e2d7c85db038.cloudfront.net (CloudFront)
X-Amz-Cf-Pop: CDG52-P1
X-Amz-Cf-Id: jRjoc9PHoiv60CnDZfF6ZXc7Kwhbxc5DxtytvFpUT-EomFPDN1OjFw==
Age: 270414
Content-Security-Policy: default-src 'none'; sandbox
```

Ah, this could no longer repro because now Cloudfront has cached this request - so the HEAD request to Cloudfront responds as expected.

The original issue is on Xet Bridge service (cas-bridge.xethub.hf.co) - maybe the issue remains that Bridge service doesn't have the appropriate CORS headers to support this request.

Also, I just tried with one of my private datasets. Not sure if it's related, on this URL I get an error, not with HEAD, but with the OPTIONS request.

```
XHR OPTIONS https://cas-bridge.xethub.hf.co/xet-bridge-us/655df24cde919d4162341a19/09ed3e86bf64d019919194d776abaa53b14acae6701129bb09f6169041b43f92?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Content-Sha256=UNSIGNED-PAYLOAD&X-Amz-Credential=cas/20260112/us-east-1/s3/aws4_request&X-Amz-Date=20260112T160451Z&X-Amz-Expires=3600&X-Amz-Signature=4562a3c72667a4bedc056c87e75a2810b52b682ebe18f7ff5b6c8d4ab081cc38&X-Amz-SignedHeaders=host&X-Xet-Cas-Uid=60a76b174e24361791fe822d&response-content-disposition=inline;+filename*=UTF-8''0000.parquet;+filename="0000.parquet";&x-id=GetObject&Expires=1768237491&Policy=eyJTdGF0ZW1lbnQiOlt7IkNvbmRpdGlvbiI6eyJEYXRlTGVzc1RoYW4iOnsiQVdTOkVwb2NoVGltZSI6MTc2ODIzNzQ5MX19LCJSZXNvdXJjZSI6Imh0dHBzOi8vY2FzLWJyaWRnZS54ZXRodWIuaGYuY28veGV0LWJyaWRnZS11cy82NTVkZjI0Y2RlOTE5ZDQxNjIzNDFhMTkvMDllZDNlODZiZjY0ZDAxOTkxOTE5NGQ3NzZhYmFhNTNiMTRhY2FlNjcwMTEyOWJiMDlmNjE2OTA0MWI0M2Y5MioifV19&Signature=SsYstbIroSWXjARDLcHQyENDwkeq0l~Nsu9RvDA-x82YSnnd8dGa0wAlYiAS2STomKmZmDtTwti3RZ4Lha2dCSnNwqHJPIiF8jFFv4h5IDXm1VKzdh~14tmA1TNfpdwSdkWCpPxTgxY2kUeOJ-qldoY20Cp9K6G-GWanKYYRft~q9mlJy5E~l-CaXnRs1PBFRPj6sci-G0aCwXtjbBjUZCg2z4--e~uwLNKJHHVeDe3wUC~GNblRNwLm-EYTzINbfpm99g3t8wHRKAQJxiXZcVUsFqcULOLiVps2NPblzJNi9Y0SCgx3buEmtm~HkZ4IsjUTJj337y4MOpmZVSEvVg__&Key-Pair-Id=K2L8F4GPSG1IFC CORS Preflight Did Not Succeed
...
Cross-Origin Request Blocked: The Same Origin Policy disallows reading the remote resource at https://cas-bridge.xethub.hf.co/xet-bridge-us/655df24cde919d4162341a19... 
```

Thanks @rajatarya, here is a link to a [Google Colab notebook](https://colab.research.google.com/drive/15soyg7g3CCdlBMDDcljeiVsq_yjeJREJ#scrollTo=bG5PfGTBK7wU) where the issue can be reproduced. The notebook tries to access a Hugging Face dataset on the front-end and it only works if we set up a proxy server to avoid the CORs issues. Otherwise we see this error in the console:

```
outputframe.html?vrz=colab-external_20260109-060046_RC02_854292615:1 Access to fetch at 'https://cas-bridge.xethub.hf.co/xet-bridge-us/695b406827b0975343f6a1a2/7f3468fcbc686cda54ec68dda14cc5f677402e9e0f540772f770b96b4a687916?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Content-Sha256=UNSIGNED-PAYLOAD&X-Amz-Credential=cas%2F20260112%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20260112T215017Z&X-Amz-Expires=3600&X-Amz-Signature=eb126a50a8f9da1bc80110f0408f1e25b764d5d71d30502f13ece6469b487921&X-Amz-SignedHeaders=host&X-Xet-Cas-Uid=public&response-content-disposition=inline%3B+filename*%3DUTF-8%27%27chunk_0.parquet%3B+filename%3D%22chunk_0.parquet%22%3B&x-id=GetObject&Expires=1768258217&Policy=eyJTdGF0ZW1lbnQiOlt7IkNvbmRpdGlvbiI6eyJEYXRlTGVzc1RoYW4iOnsiQVdTOkVwb2NoVGltZSI6MTc2ODI1ODIxN319LCJSZXNvdXJjZSI6Imh0dHBzOi8vY2FzLWJyaWRnZS54ZXRodWIuaGYuY28veGV0LWJyaWRnZS11cy82OTViNDA2ODI3YjA5NzUzNDNmNmExYTIvN2YzNDY4ZmNiYzY4NmNkYTU0ZWM2OGRkYTE0Y2M1ZjY3NzQwMmU5ZTBmNTQwNzcyZjc3MGI5NmI0YTY4NzkxNioifV19&Signature=fZCXLc5VxfW8xKDcKK2R4CbPAVNAUgnMUjBrcrGNYebOaDMrsyjqs5bgKUcI9P67jYGLTowltxypbKrlA8UDfwKFtdT9GUtQXjAUNY%7ENjoKUacJXFuJtBeucdef5dnRu%7E4HC%7ECz2NJu69qCh0QNGNk9BH0D-83CptPhNuUHGZ%7EgT9F%7Ehe5RZR3bTSNg-6K6DSqx3JtxUu4-P5ZWSqwq4SuqAatm0019euel2wCciWc7HbYQ3b2XXkQAjLXvgLpuP-y-2JOkvG3SDJXoPamzH-wkKmBTdmCLxw%7ENHLi3F9w%7EfpSWZMn-61KR48g5E9LahtGPbRvti8Nvs-qES441DXA__&Key-Pair-Id=K2L8F4GPSG1IFC' (redirected from 'https://huggingface.co/datasets/cornhundred/Celldega_Xenium_Prime_Ovarian_Cancer_FFPE_XRun_outs_row_groups_image_chunk/resolve/main/Xenium_Prime_Ovarian_Cancer_FFPE_XRrun_outs/transcripts/chunk_0.parquet') from origin 'https://fjy31o3wnuf-496ff2e9c6d22116-0-colab.googleusercontent.com' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: It does not have HTTP ok status.
```

@cornhundred Sorry for the delay in getting back to you on this. Got wrapped up in other things and just got back to these changes earlier this week. I have a PR updated with some tests. I am a little nervous with deploying these changes without more testing - we don't have automated testing in place for CORS changes given security policies on private network access preventing our scenrio tests from running.

For deploying these changes I will likely want to work with folks on the HF side (@coyotte508 @severo @lhoestq) to do a coordinated deployment so we can manually test these changes in real-time and verify we didn't regress existing behavior (and verify new behavior is working as expected as well).

Hi @rajatarya, thank you for the update. 

For info - the `@huggingface/hub` JS lib has a `downloadFile` function that supports ranged requests. It doesn't go through the bridge:

```ts
import { downloadFile } from `@huggingface/hub`;

const blob = await downloadFile({repo: ..., path: ..., accessToken: ...});
const slice = (await blob.slice(X, Y)).toArrayBuffer(); // or can .stream() on a .slice() too
const slice2 = (await blob.slice(X2, Y2)).toArrayBuffer();
```

Although, if the bridge works with CORS it might be faster for you (since it's very close to the xet backend, the various http calls it makes to the xet backend will have less latency)

Apologies for not mentioning it earlier

@cornhundred : We deployed fixes on our backend and believe this issue should be fixed. The Colab notebook no longer reproduces this error. Please let me know if you continue to see this issue. Thanks for your patience with me as we got this issue addressed.

The initial CORS changes planned turned out to be insufficient because the OPTIONS request was failing presigned URL validation due to the presigned URL being for a GET request (to download the full file) but the browser was issuing an OPTIONS request as preflight prior to the GET (with Range header) request. The OPTIONS request was returning 401 Unauthorized, which was showing up in the Console as 'not returning http ok status'.

Thanks @rajatarya!! Yes, I can confirm that it is working on my end. I very much appreciate the help with this!!