Hugging Face Spaces proxy suddenly stripping Access-Control-Allow-Credentials header on OPTIONS preflight?

Hey everyone,

I’ve had a full-stack MERN app running perfectly for months. The backend is hosted on Hugging Face Spaces (express server in a Docker container), and the frontend is on Vercel.

Out of nowhere, my /user/login route started failing with a CORS error: The value of the 'Access-Control-Allow-Credentials' header in the response is '' which must be 'true' when the request's credentials mode is 'include'.

When inspecting the Network tab, I can see that the browser sends an OPTIONS preflight request, and the response headers from the backend look like this:

HTTP

access-control-allow-headers: content-type
access-control-allow-methods: POST
access-control-allow-origin: https://iskra-edu.vercel.app
access-control-max-age: 600
content-length: 0
vary: origin, access-control-request-method, access-control-request-headers

As you can see, Access-Control-Allow-Credentials is completely missing.

The catch: My Express code explicitly has credentials: true configured inside the cors middleware, and I even added a manual global wildcard middleware at the very top of my app to force-inject the header on all OPTIONS requests:

JavaScript

app.use((req, res, next) => {
    res.setHeader('Access-Control-Allow-Credentials', 'true');
    if (req.method === 'OPTIONS') return res.sendStatus(200);
    next();
});

Even with this, the header never reaches the browser. It seems like the Hugging Face edge proxy/routing mesh is intercepting the OPTIONS request and stripping out the Access-Control-Allow-Credentials header before it can hit my container, or it’s answering the preflight entirely on its own.

Has anyone else experienced Hugging Face randomly breaking preflight CORS headers recently? Is there a new configuration in README.md or the routing mesh that I missed? Any help or workaround (besides bypassing preflight via URL-encoded forms) would be highly appreciated!

Hmm… the symptom itself feels like an HF-side glitch, but I wonder if this series of glitches may be connected somehow​:thinking::
just in case, @hysts


I cannot prove that this shares a root cause with other recent Spaces issues, so I would treat this only as a hypothesis.

But your specific CORS symptom looks suspicious in a useful way: the OPTIONS preflight response is not missing all CORS headers. It seems to include several CORS headers, but Access-Control-Allow-Credentials: true is missing even though your Express app is configured to return it.

That makes me wonder whether this particular OPTIONS path is being answered, normalized, cached, or rewritten before it reaches your container.

The first thing I would try to establish is:

  • does OPTIONS /user/login show up in your container logs?
  • if yes, does your container actually return Access-Control-Allow-Credentials: true?
  • if no, Spaces Proxy / edge logic may be answering the preflight itself
  • if yes but the browser/curl response does not receive it, something between the container and browser may be stripping or rewriting the header

A direct preflight check would be useful:

curl -i -X OPTIONS "https://<space-subdomain>.hf.space/user/login" \
  -H "Origin: https://<frontend-origin>" \
  -H "Access-Control-Request-Method: POST" \
  -H "Access-Control-Request-Headers: content-type"

For credentialed CORS, I would expect the response to include something like:

Access-Control-Allow-Origin: https://<frontend-origin>
Access-Control-Allow-Credentials: true

If the app logs do not show the OPTIONS request, or if the app returns Access-Control-Allow-Credentials: true but the browser/curl response does not contain it, then this is probably not just an Express middleware issue.

I also noticed several nearby Spaces reports that may fit the same broad class of “app/build/container is healthy, but HF-side state, active deployment, route binding, service metadata, or proxy path is inconsistent.” I am not saying they are definitely the same bug, but the pattern seems worth checking.

Longer evidence map / possible HF-side consistency issue

Working hypothesis

I would not frame this as:

HF definitely broke CORS globally.

A safer framing is:

This CORS/preflight issue may be an edge symptom of a broader Spaces state / route / proxy consistency problem.

The common shape across these reports is not simply:

The user app crashed.

It is closer to:

The app, build, or container may be healthy, but the HF-side state, active deployment pointer, runtime SHA, service metadata, route binding, health/status reflection, or proxy path appears inconsistent.

That distinction matters because it changes what to inspect.

If this is app-side CORS, the fix is probably in Express middleware.

If this is proxy-side preflight handling, the relevant questions are:

  • whether OPTIONS is forwarded to the container
  • whether container response headers are preserved
  • whether Access-Control-Allow-Credentials is stripped or omitted by a proxy/header allowlist/sanitizer
  • whether OPTIONS uses the same active service binding as normal GET / POST

Useful background references:

Evidence map

Bucket Symptom pattern Example reports Why it may matter here
Runtime state stale App responds, but Space status or runtime SHA remains stale Space stuck on “Starting” badge despite app running fine on port 7860, Docker Space stuck in APP_STARTING with stale runtime SHA Suggests container/app health and HF-side runtime state can diverge
Deployment cutover failure New builds succeed, but live traffic keeps going to old behavior Space stuck “Restarting” on an old commit for 16+ hours — new builds succeed but never get traffic Suggests active deployment / route binding can fail to update
Space record / service metadata issue metadata / compute / service undefined, delete/recreate/rename issues 400 Error on newly created Space, Please force-delete a broken Space — stuck “Building” / 400 on all operations, Docker Space Build Stuck — Empty Logs, Why I can not delete my space?, Cannot Delete Space, I can’t delete or do anything to this space Suggests Space metadata/control-plane records can become incomplete or inconsistent
Health/status reflection Direct .hf.space URL works, but Hub UI remains Starting Space stuck at “Starting” - health checker not responding - June 20 2026, Space stuck on “Starting” indefinitely with completely clean startup log Suggests app readiness and Hub-side status can diverge
Proxy/backend forwarding Container is running, but requests do not reach it Docker Spaces returning {"data":[]} — proxy not forwarding requests to container, 503 shown for website for some request, while server is running and requests are being served Very relevant if OPTIONS is being answered by proxy or routed differently from normal requests
External integration / subdomain lookup Direct Space works, but Web Component / by-subdomain path fails Gradio web components failing to load, Gradio issue #12570, HF docs: Embed your Space Suggests external browser-facing integration paths can fail outside the user app
Runtime-control / lifecycle Build may succeed, but restart/factory reboot/resume fails Space stuck in broken runtime-control state — build succeeds, but factory reboot returns 503 and restart API fails, Docker Space stuck in “Build Queued” / immediately “Paused”, Stuck at space problem Suggests some failures live in lifecycle/runtime-control rather than app code

Case notes / observed timelines

These are not proof of one shared root cause. I think they are still useful because each case contains more than one symptom within the same Space, same user, or same operation chain.

Space / thread Observed sequence Why it is useful
anandkr/Ama Backend New commits build successfully → container logs show clean startup → dashboard remains on old commit / Restarting → live endpoint keeps serving old behavior → Restart / Factory Rebuild do not cut over Strong example of build success but active deployment / live route not updating. Thread
juakazike/gender-sensitization-engine App responds / HTTP 200 → Gradio UI/API works → badge remains Starting → runtime API SHA is stale compared with Space SHA Strong example of app health diverging from HF-side runtime/status state. Thread
kayinda/rxsteward Space stuck Building → delete fails → API delete_repo fails → recreating/renaming to same name fails → metadata / compute / service undefined → HF staff says infra team shared/resolved Strong example of Space record / lifecycle state corruption. Thread
yc-lin-geo/ISMIP6-Antarctica-SLR-Explorer Build stuck / empty logs → delete/recreate with same name → same-name Space still returns metadata/compute/service undefined Useful because the bad state appears to persist across delete/recreate cycles with the same name. Thread
rajvivan Docker Spaces proxy case Container startup complete → Uvicorn listens on 0.0.0.0:7860 → incoming request logs stay empty → public route returns {"data":[]} Directly relevant to proxy/backend forwarding; request path may not reach the container. Thread
June 20 Starting cluster Container logs show app listening → direct .hf.space works for some users → Hub UI remains Starting → HF staff says they will check internally → related users report minimal / clean startup examples with the same symptom Useful as a time-near health/status reflection cluster. Thread
March metadata / compute / service cluster Settings/delete/create operations fail with metadata, compute, storage, or service undefined Useful as older evidence that Space metadata/control-plane records can fail in this shape. Example 1, Example 2, Example 3
Web Component / by-subdomain case Direct HF Space loads fine → Web Component path fails → console calls /api/spaces/by-subdomain/<space-subdomain> → API returns invalid repo name / encoded slash style error Useful as another external browser-facing path that can fail outside the user app. Thread, Related Gradio issue

Why I think the CORS symptom is interesting

This CORS case is not simply:

No CORS headers are returned.

It is more specific:

Some CORS headers are returned,
but Access-Control-Allow-Credentials is missing.

That is an important distinction.

If Spaces Proxy or edge logic has a special fast path for OPTIONS, then the response might be generated before the request reaches the app.

Or, if the app response is passed through but normalized, a header allowlist/sanitizer could drop Access-Control-Allow-Credentials.

That would produce exactly the kind of failure where:

  • Access-Control-Allow-Origin exists
  • Access-Control-Allow-Methods exists
  • Access-Control-Allow-Headers exists
  • Access-Control-Max-Age exists
  • but Access-Control-Allow-Credentials is missing

This would also explain why changing Express middleware may not help.

Checks that would separate app-side CORS from proxy-side CORS

Check Result Interpretation
Add top-level request logging before all Express middleware OPTIONS /user/login appears Request reaches container
Add top-level request logging before all Express middleware OPTIONS /user/login does not appear Preflight may be answered before container
Log response headers from the app for OPTIONS App includes Access-Control-Allow-Credentials: true App-side config is probably not the missing piece
Compare app-side headers vs curl -i -X OPTIONS response Header exists in app but not curl/browser Header may be stripped/rewritten after container
Compare GET/POST logs with OPTIONS logs GET/POST reach container, OPTIONS does not Method-specific proxy/preflight path likely
Duplicate the Space under a different name Bug disappears Possible Space record / route state issue
Duplicate the Space and bug persists More likely platform-level behavior or app config
Test from another frontend origin Only some origins fail Origin normalization / allowlist / cache issue possible
Test after waiting past Access-Control-Max-Age Old behavior persists Less likely browser preflight cache only

Minimal app-side logging snippet

For Express, I would add this before cors() and before all route handlers:

app.use((req, res, next) => {
  console.log("INCOMING", req.method, req.path, req.headers.origin);
  next();
});

If the log never shows OPTIONS /user/login, the app cannot fix that preflight response because it never sees it.

If the log shows OPTIONS /user/login, then add a very explicit handler temporarily:

app.options("/user/login", (req, res) => {
  console.log("OPTIONS handler reached", req.headers.origin);

  res.setHeader("Access-Control-Allow-Origin", "https://<frontend-origin>");
  res.setHeader("Access-Control-Allow-Credentials", "true");
  res.setHeader("Access-Control-Allow-Methods", "POST, OPTIONS");
  res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization");
  res.setHeader("Vary", "Origin");

  return res.status(204).end();
});

Then compare what the app logs with what this command receives:

curl -i -X OPTIONS "https://<space-subdomain>.hf.space/user/login" \
  -H "Origin: https://<frontend-origin>" \
  -H "Access-Control-Request-Method: POST" \
  -H "Access-Control-Request-Headers: content-type"

Possible HF-side checks

If someone from HF checks this, these would probably narrow it quickly:

  • Is the OPTIONS request forwarded to the user container?
  • If forwarded, are response headers from the container preserved?
  • Is Access-Control-Allow-Credentials stripped by a proxy/header allowlist/sanitizer?
  • Is OPTIONS using the same active service binding as normal GET / POST?
  • Does the active runtime SHA match the latest successful build?
  • Does the public .hf.space route point to the expected runtime?
  • Is the Space service metadata record complete?
  • Do these paths resolve to the same active service?
    • Hub viewer
    • direct .hf.space
    • Web Component / embed
    • by-subdomain API
    • CORS preflight
    • normal GET / POST

What I would avoid mixing in

I would keep explicit abuse/moderation cases in a separate bucket unless there is a clear abuse-handler, Cloudflare rule, or moderation flag.

Those can produce similar symptoms such as pause/restart failure, but that is a different mechanism from a suspected state/route/proxy consistency glitch.

Cautious conclusion

I cannot prove one root cause across all of these threads.

But I would not dismiss this CORS report as a normal Express CORS misconfiguration until the OPTIONS path is proven to reach the container and preserve the container’s headers.

The most useful framing may be:

Could this be another edge symptom of a Spaces state / route / proxy consistency regression, especially around OPTIONS preflight handling and active service binding?

Small update:

This related Web Component reply may be relevant: Gradio web components failing to load - #4 by John6666

The key clue is that the issue may not be limited to by-subdomain. Even the explicit space="<owner>/<space_name>" form reportedly generated:

https://.hf.space/config

That looks like a missing resolved Space host, not a user app/runtime problem.

I still would not claim the same root cause, but it fits the same broad pattern: the direct app/runtime may be fine, while a browser-facing Spaces integration path is inconsistent.

For Web Components, that suspicious path is roughly:

owner/name or subdomain
  -> resolved Space host
  -> /config

For this CORS issue, it may be:

OPTIONS preflight
  -> Spaces Proxy / edge / active service binding
  -> CORS headers

So this may be worth checking as part of the same Space identity / host resolution / active service binding layer, rather than only as an app-side CORS middleware issue.

Same issue.

I also have an express and frontend on vercel. The GET request works normally but all the other requests - POST, PATCH, PUT … doesnt work.

How can we tag them?

Oh. Some time after my post above, I found evidence that this CORS-related behavior change is probably closer to a de facto platform policy than to a transient outage:


Regarding your case: it may be the same class of problem, especially if your POST/PATCH/PUT requests use cookies or credentials: "include" / withCredentials: true, but “GET works” is not enough to establish that yet.

I should also revise part of my earlier reply. I initially suspected that this might share a root cause with broader Spaces routing/state-consistency incidents. The later Gradio investigation and fix points to a more specific explanation:

  • the Spaces reverse proxy had received a security hardening change;
  • cross-origin responses were no longer receiving Access-Control-Allow-Credentials;
  • Gradio’s JS client had been sending every request with credentials: "include";
  • Gradio therefore changed its default to credentials: "same-origin" and removed unnecessary headers that were causing extra preflights.

The same PR explicitly says that the by-subdomain 400 error discussed in the associated Gradio issue was a separate Hub-side problem. Therefore, my earlier broad common-root-cause hypothesis should be downgraded.

I have not found a formal Hugging Face policy or configuration entry documenting this as a permanent Spaces API contract in the current Spaces changelog or Spaces configuration reference. Still, the strongest public evidence currently available treats it as an intentional security boundary, and Gradio has adapted its client around it. For practical purposes, I would therefore not assume that credentialed browser requests from Vercel directly to *.hf.space will start working again after an outage is fixed.

The default route I would use is:

Situation Most practical direction
The API does not actually need a cross-origin browser Cookie Remove credentials: "include" / withCredentials: true and use an uncredentialed request
Browser-side Cookie authentication is essential Put a same-origin backend-for-frontend or server-side relay on the Vercel side
You are not using credentials at all Check ordinary preflight configuration, allowed methods/headers, redirects, and the real backend error
Only one method or route fails Compare the exact request shape and whether that request reaches Express

Since @hysts has already been tagged above, I suspect that another tag by itself will be less useful than attaching one small, reproducible comparison: the exact frontend request, the public OPTIONS response, and whether the same OPTIONS request appears in the container logs.

The fastest useful checks are:

  1. Compare the actual GET and POST/PATCH/PUT request configurations.
  2. Send the preflight manually.
  3. Check whether Express receives it.
  4. Compare credentialed and uncredentialed requests against a harmless probe route.
  5. Try the actual request server-to-server, where browser CORS enforcement is absent.

For example:

curl -sv --max-redirs 0 \
  -X OPTIONS "https://<space-subdomain>.hf.space/<route>" \
  -H "Origin: https://<frontend-origin>" \
  -H "Access-Control-Request-Method: POST" \
  -H "Access-Control-Request-Headers: content-type" \
  -o /dev/null

If your browser request also sends Authorization or another non-safelisted header, include it in Access-Control-Request-Headers.

For a credentialed request, the relevant response normally needs at least:

Access-Control-Allow-Origin: https://<frontend-origin>
Access-Control-Allow-Credentials: true
Access-Control-Allow-Methods: POST
Access-Control-Allow-Headers: content-type

The browser’s preflight request itself normally contains no Cookie. That is expected. The preflight response authorizes the browser to send the subsequent credentialed request. See the MDN CORS guide and Access-Control-Allow-Credentials reference.

Why I am revising my earlier hypothesis

The closest public case I found is the Gradio Web Component incident reported in gradio-app/gradio#13554.

The symptoms included two different failures:

  1. a Hub API by-subdomain lookup returning a 400 error;
  2. direct cross-origin requests to a Space such as /config failing under CORS.

The follow-up PR #13581 separated those two failures.

According to the PR:

  • the by-subdomain failure was a distinct Hub-side issue and had already been fixed;
  • the remaining CORS failures were triggered by a security hardening change in the Spaces reverse proxy;
  • the proxy no longer supplied Access-Control-Allow-Credentials to cross-origin responses;
  • the old Gradio JS client sent all requests using credentials: "include", even for public Spaces where no Cookie was required.

Gradio changed the default request mode from:

credentials: "include"

to:

credentials: "same-origin"

With same-origin, Cookies are still sent when a Gradio application is accessed from its own origin, but a third-party page embedding a public Space does not attempt a credentialed request to *.hf.space.

Gradio also removed Content-Type: application/json from bodyless GET requests such as /config. That header served no purpose on a GET with no body, but it could cause a preflight. Removing it kept those requests CORS-safelisted where possible.

There is an important nuance: Gradio did not declare credentialed CORS invalid in every deployment. During review, an explicit credentials option was retained for self-hosted deployments that intentionally configure credentialed CORS:

const client = await Client.connect("https://ml-app.example.com", {
  credentials: "include"
});

That distinction suggests:

  • credentialed CORS remains a valid web architecture when both origins and the server are intentionally configured for it;
  • the current restriction appears specific to the browser-to-Spaces proxy boundary;
  • changing Express alone may not override that higher-level Spaces policy.

The PR also describes arbitrary third-party-origin embeds of private or auth-gated Spaces, which relied on cross-origin session Cookies, as remaining blocked by design. It suggests that supporting such access would require an explicit server-side mechanism such as an origin allowlist or token-based flow.

That evidence is considerably more specific than my earlier idea that this was another symptom of a general Space state, active deployment, or route-binding inconsistency.

What the available evidence establishes — and what it does not

Reasonably supported

The following interpretation is supported by the Gradio PR and its merged implementation:

  • a Spaces proxy security change affected credentialed cross-origin requests;
  • Gradio maintainers treated the behavior as intentional rather than waiting for a proxy rollback;
  • public cross-origin embeds were adapted to send no Cookies;
  • arbitrary third-party-origin Cookie access to private/auth-gated Spaces was treated as intentionally blocked;
  • the associated by-subdomain problem was separate.

Not established

The public evidence does not establish all of the following:

  • that this is a formally documented and permanent Hugging Face API guarantee;
  • that the proxy literally removes every user-generated Access-Control-Allow-Credentials header;
  • that OPTIONS is always generated at the edge instead of reaching the container;
  • that all Docker Spaces, visibility modes, routes, and custom domains are handled identically;
  • that there cannot be narrower bugs in the implementation of the policy.

For example, the policy may be intentional while one of these remains a platform bug:

  • an anonymous, uncredentialed request is accidentally blocked;
  • a public Docker Space receives inconsistent handling by method;
  • the container emits one header set but the public response exposes another;
  • a cached preflight response is reused across origins incorrectly;
  • a documented public/protected embed flow stops working;
  • the rule is applied outside the scope intended by the security change.

Therefore, I would avoid both extremes:

This is definitely a temporary bug.

and:

Every observed behavior is necessarily correct because it is a security policy.

A more supportable position is:

The absence of credential support should not currently be assumed to be a transient outage, but the way that restriction is applied can still be tested for narrower implementation problems.

How to determine whether your GET and mutation failures have the same cause

“GET works, but POST/PATCH/PUT do not” is consistent with a preflight boundary, but it is not uniquely diagnostic.

A GET may work because:

  • it does not use credentials;
  • it has no non-safelisted headers and avoids preflight;
  • it calls a public route while mutations call authenticated routes;
  • GET and mutations use different Axios/fetch clients;
  • “works” means it succeeds in curl, Postman, or the address bar rather than browser JavaScript;
  • the network request completes but its response is not exposed to JavaScript;
  • the mutation request fails in the application after CORS succeeds.

A credentialed GET that is read successfully by browser JavaScript still requires Access-Control-Allow-Credentials: true on its actual response, even when no preflight occurs. Therefore, if the same frontend sends credentials: "include" on both GET and POST and the GET response is genuinely readable in JavaScript, the two requests may not be following exactly the same path.

Compare the exact request shapes

A useful comparison is:

Property GET POST/PATCH/PUT
Full URL
Origin
credentials
Axios withCredentials
Content-Type
Authorization
Other custom headers
Redirect
Route requires authentication

For example, these are materially different requests:

fetch(url, {
  method: "GET"
});
fetch(url, {
  method: "POST",
  credentials: "include",
  headers: {
    "Content-Type": "application/json"
  },
  body: JSON.stringify(data)
});

The latter normally requires a preflight and also asks the browser to use credentials.

Compare edge and container observations

Add temporary logging before cors() and before authentication middleware:

app.use((req, res, next) => {
  const startedAt = Date.now();

  res.on("finish", () => {
    console.log({
      method: req.method,
      path: req.originalUrl,
      origin: req.headers.origin,
      status: res.statusCode,
      allowOrigin: res.getHeader("access-control-allow-origin"),
      allowCredentials: res.getHeader(
        "access-control-allow-credentials"
      ),
      allowMethods: res.getHeader("access-control-allow-methods"),
      allowHeaders: res.getHeader("access-control-allow-headers"),
      durationMs: Date.now() - startedAt
    });
  });

  next();
});

Do not log passwords, session Cookies, authorization tokens, or full login bodies.

Then compare the same timestamp and request:

Observation Likely implication
OPTIONS never appears in Express Something before the container may be answering or rejecting it
OPTIONS reaches Express The preflight is at least forwarded to the app
Express reports ACAC, public curl response does not A layer after Express may be altering or replacing the response
Express itself does not report ACAC Recheck middleware placement and response handling
OPTIONS succeeds, actual POST returns 401/403 Authentication, Cookie, or CSRF is the next layer
curl POST works, browser POST fails Browser CORS/Cookie policy becomes more likely
both browser and curl POST fail The route itself may have an application error

The official cors middleware documentation is useful here. When cors() is applied at application level, it normally handles preflight requests for all routes. Its credentials: true option normally emits Access-Control-Allow-Credentials: true.

Use a harmless control route

Rather than repeatedly testing /user/login, a no-side-effect route makes comparison safer:

app.get("/cors-probe", (req, res) => {
  res.json({ ok: true, method: "GET" });
});

app.post("/cors-probe", (req, res) => {
  res.json({ ok: true, method: "POST" });
});

Then compare:

  1. GET without credentials;
  2. GET with credentials;
  3. JSON POST without credentials;
  4. JSON POST with credentials.

This separates two questions:

  • Does the route require a preflight?
  • Does adding the credentials mode cause the failure?

Test the actual backend outside browser CORS

A server-to-server request does not prove that browser CORS is configured correctly, but it can reveal the backend’s real status:

curl -sv --max-redirs 0 \
  -X POST "https://<space-subdomain>.hf.space/<harmless-probe-route>" \
  -H "Origin: https://<frontend-origin>" \
  -H "Content-Type: application/json" \
  --data '{"probe":true}' \
  -o /dev/null

If this returns 401, 403, 404, 405, or 500, the browser may be reporting a CORS symptom while hiding a separate application error.

Compact interpretation table

Result First branch to investigate
Uncredentialed GET and POST work; credentialed versions fail Spaces credential policy / browser Cookie boundary
GET works; JSON POST preflight fails Allowed methods/headers or credential policy
OPTIONS never reaches Express Edge/proxy handling
OPTIONS reaches Express and app emits ACAC, but public response omits it Post-container proxy behavior
OPTIONS succeeds but POST is 401/403 Authentication, session Cookie, or CSRF
Production domain works but Vercel preview fails Origin allowlist or deployment protection
Browser fails but Vercel server-side request works Move browser-facing boundary to Vercel
Different origins receive a cached ACAO value Cache key or missing Vary: Origin
Practical routes depending on what the application actually needs

A. The endpoint does not need a browser Cookie

This is the simplest and most robust branch.

Use an uncredentialed request:

fetch(url, {
  method: "POST",
  credentials: "omit",
  headers: {
    "Content-Type": "application/json"
  },
  body: JSON.stringify(data)
});

or leave credentials at its default where appropriate.

For Axios:

axios.post(url, data, {
  withCredentials: false
});

Also avoid unnecessary request headers. A bodyless GET generally does not need:

Content-Type: application/json

Removing unnecessary non-safelisted headers may avoid an otherwise unnecessary preflight. That is one of the changes made in the Gradio fix.

This is suitable for public/anonymous API operations. It is not sufficient for a login route whose purpose is to establish and use a cross-origin session Cookie.

B. Browser-side Cookie authentication is essential

In that case, I would not make the browser call *.hf.space directly and depend on cross-origin Cookies.

A more controllable shape is:

Browser
  -> https://frontend.example.com/api/*
  -> Vercel Route Handler / server function / BFF
  -> https://<space-subdomain>.hf.space/*

The browser communicates with its own origin. The Vercel-side component then communicates server-to-server with the Space.

This is more than a transparent network proxy. The BFF becomes the component that owns or translates authentication:

Browser <-> Vercel:
    frontend session Cookie

Vercel <-> HF Space:
    server-held token,
    explicit upstream session,
    or another application-specific credential

The BFF should validate the user and authorize each operation before forwarding it. It should not blindly forward every incoming Cookie or header to the Space.

If the frontend is Next.js, the official Backend for Frontend guide shows using a Route Handler as a proxy with validation before forwarding the request.

C. You are using an Authorization header instead of Cookies

This avoids some Cookie-specific constraints, but it does not automatically avoid CORS.

A browser-supplied Authorization header normally causes a preflight, and the preflight response must allow that header:

Access-Control-Allow-Headers: authorization, content-type

Also, a private Hugging Face access token should not be embedded in a public frontend bundle. Keep service credentials server-side and scope them as narrowly as possible; see the HF User access token documentation.

The Gradio PR’s statement that its “token path is unaffected” refers to Gradio’s own client/authentication flow. It should not be generalized into “put an HF token in browser JavaScript.”

D. No credentials are involved

Then this may be an ordinary preflight or application configuration problem rather than the Spaces credential restriction.

Check:

  • whether POST/PATCH/PUT appear in Access-Control-Allow-Methods;
  • whether all requested headers appear in Access-Control-Allow-Headers;
  • whether OPTIONS returns 200 or 204 rather than 404/405;
  • whether CORS middleware runs before authentication and route handlers;
  • whether error responses also receive CORS headers;
  • whether the request is redirected;
  • whether the returned Access-Control-Allow-Origin exactly matches the browser’s Origin;
  • whether Vary: Origin is present when the origin is selected dynamically.

Do not use Access-Control-Allow-Origin: * together with credentialed requests. Credentialed CORS requires an explicit allowed origin.

Caveats if you add a Vercel rewrite or backend-for-frontend

A Vercel-side boundary is likely the most practical long-term route when browser Cookie authentication is essential, but it introduces its own constraints.

External rewrite versus application-level BFF

Vercel supports rewrites to external origins, for example:

{
  "rewrites": [
    {
      "source": "/api/:path*",
      "destination": "https://<space-subdomain>.hf.space/:path*"
    }
  ]
}

This can be a useful small control:

  • browser calls the Vercel origin;
  • Vercel forwards to the Space;
  • the browser no longer directly crosses from Vercel to hf.space.

However, a simple rewrite gives less control over:

  • which Cookies are accepted;
  • how the user is authenticated;
  • how authentication is translated upstream;
  • CSRF protection;
  • Set-Cookie rewriting;
  • redirects;
  • error normalization;
  • per-route authorization;
  • header filtering;
  • logging and auditability.

For authenticated production routes, an application-level BFF/Route Handler is often easier to reason about than a transparent rewrite.

Caching

Current Vercel rewrite documentation says that external rewrites may honor upstream:

Cache-Control
CDN-Cache-Control
Vercel-CDN-Cache-Control

For projects created on or after April 6, 2026, this behavior may be enabled by default.

Authentication responses, user-specific data, login responses, and mutation results should not be accidentally cached. Verify the effective cache headers and use an explicit no-store policy where required.

Also verify Vary: Origin if any cross-origin response is cached. A response generated for one Origin should not be served with the same CORS metadata to another Origin.

Payload and duration limits

A Vercel Function currently has a 4.5 MB request/response payload limit. If the POST/PATCH/PUT routes transfer images, audio, large files, or large generated responses, routing the full payload through a Function may turn the CORS issue into a 413 or oversized-response problem.

Current Vercel documentation also lists a 120-second maximum for requests proxied to an external destination. Long-running inference, uploads, streaming, or queue operations should be checked against the relevant Vercel platform limits.

A rough split is:

Workload Likely route
Small JSON login/mutation request BFF is a reasonable candidate
Large upload Direct object-storage upload or signed upload path
Large generated file Store externally and return a reference
Long inference Queue/job model, streaming with verified limits, or polling
User-specific mutation Explicitly disable CDN caching

Preview deployment origins

Vercel creates unique generated URLs for deployments and branches. Therefore:

https://app.example.com
https://project-git-branch-team.vercel.app
https://project-<hash>-team.vercel.app

are different origins.

A production allowlist may work while previews fail. Conversely, reflecting every *.vercel.app origin without validation is usually too broad for credentialed routes.

Use an explicit policy for:

  • production origin;
  • approved branch preview origins;
  • local development origin;
  • whether protected preview deployments are expected to call the API.
Browser Cookie limitations remain relevant even if the proxy changes again

CORS permission and Cookie acceptance are related but separate layers.

Even if a server returns:

Access-Control-Allow-Origin: https://frontend.example
Access-Control-Allow-Credentials: true

the browser may still restrict a Cookie because it is cross-site or third-party.

Depending on the design, a cross-site Cookie generally needs appropriate attributes such as:

SameSite=None; Secure

and the browser may still apply its third-party Cookie policy.

The MDN CORS guide notes that third-party Cookie policies apply independently of the CORS headers. Hugging Face also documents general Cookie limitations in Spaces, because the Hub page and the application hosted on *.hf.space are different domains.

Therefore, even if Access-Control-Allow-Credentials reappeared later, a design based on a Vercel page directly using a session Cookie belonging to hf.space could remain browser-dependent and fragile.

This is another reason to prefer a same-origin browser session with a server-side relay when the application needs reliable authenticated mutations.

I would not work around this by:

  • switching to mode: "no-cors" — the response becomes opaque and unusable as a normal API response;
  • converting everything to URL-encoded forms solely to avoid preflight;
  • disabling CSRF protection without replacing it with another control;
  • reflecting arbitrary Origins;
  • putting an HF access token in public frontend JavaScript.
Optional architecture alternatives

These are possible design options, not confirmed fixes for this specific proxy behavior.

Custom domain

Hugging Face supports custom domains for Spaces on eligible plans. The current documentation says custom domains require public or protected visibility and are not supported for private Spaces.

A custom domain may help create a more coherent site boundary, but it does not automatically make two origins identical:

app.example.com
api.example.com

are usually same-site but still cross-origin. CORS remains relevant because scheme, host, and port define an origin.

Also, the available public information does not establish whether the Spaces credential policy behaves differently on custom domains. That would need a small probe before treating it as a workaround.

Sign in with Hugging Face

If HF identity fits the application, Hugging Face provides an official OAuth/OpenID Connect flow for Spaces and external applications.

That may be useful when:

  • users can authenticate with their HF accounts;
  • the required scopes fit the application;
  • replacing the current custom login flow is acceptable.

It is not a drop-in repair for an existing Express username/password session shared cross-origin between Vercel and hf.space.

Move the browser-facing API boundary

For a full-stack application, another possibility is to host the browser-facing API on the same platform/domain as the frontend, and use the Space only for the workloads for which Spaces is needed.

For example:

Browser
  -> Vercel application API
      -> database/authentication services
      -> HF Space only for inference or specialized compute

This keeps user authentication and mutable application state out of the browser-to-Space CORS boundary.

Related cases that should not all be merged into one root cause

The following cases are useful for comparison, but they should remain separate unless request-level evidence connects them.

Closest case

The closest public case is the Gradio cross-origin embed failure addressed in:

It directly concerns credentialed cross-origin requests to *.hf.space after a proxy security change.

Separate issue in the same report

The by-subdomain 400 failure occurred in the same Gradio issue, but the PR states that it was a separate Hub-side issue and had already been fixed.

This is why I no longer think the broad Space identity/route/state theory is the best explanation for the missing credentials header.

Similar request pattern, potentially different cause

“GET succeeds while another method fails” can also arise from:

  • Content-Type: application/json;
  • an Authorization header;
  • PATCH or PUT requiring preflight;
  • missing allowed methods;
  • missing allowed headers;
  • an OPTIONS route returning 404/405;
  • a redirect during preflight;
  • an authenticated mutation returning 401/403;
  • an error response missing CORS headers;
  • a Vercel preview origin not included in the allowlist.

These cases can look similar in the browser console while having different fixes.

General Cookie constraints

HF’s Cookie limitations in Spaces are relevant background, but that page primarily discusses the Hub/iframe/domain structure. It is not direct documentation of the June 2026 proxy change.

What would make this worth escalating as a narrower HF platform bug

The missing credential support itself now appears policy-like enough that I would not report only:

Access-Control-Allow-Credentials is missing; please restore it.

A narrower report becomes stronger if one of the following can be demonstrated:

  • the same anonymous request works uncredentialed on one method but is incorrectly blocked on another;
  • an OPTIONS response generated by Express contains ACAC, but the public response does not;
  • the container never receives OPTIONS although the platform is expected to forward it;
  • public/protected anonymous access documented by HF is broken;
  • different origins receive an incorrectly cached CORS response;
  • the proxy returns inconsistent Access-Control-Allow-Methods or Access-Control-Allow-Headers;
  • the result changes between equivalent Spaces or custom-domain/direct-domain paths;
  • the behavior affects requests that do not use credentials;
  • the platform returns a misleading success preflight but blocks the actual request inconsistently.

A compact escalation packet would contain:

Space URL:
Space visibility: public / protected / private
Frontend origin:
Production or Vercel preview:
UTC timestamp:
Route and method:
Expected result:
Observed result:
fetch/Axios request configuration:
OPTIONS curl output:
Actual-request curl output:
Container log for the same timestamp:
Credentialed control result:
Uncredentialed control result:
Relevant request ID / response headers:

Since @hysts is already tagged in this thread, posting that comparison here would probably be more actionable than tagging several unrelated maintainers.

Bottom line

I would currently treat the situation this way:

  1. Do not assume this is a temporary CORS outage that will restore browser-to-Space credentialed Cookies.
  2. Do not assume your case is identical until the GET and mutation request shapes are compared.
  3. If removing credentials makes the same POST/PATCH/PUT work, the new Spaces credential boundary is the leading explanation.
  4. If no credentials are involved, investigate ordinary preflight and the actual backend status.
  5. If Cookie authentication is essential, a same-origin Vercel BFF/server-side relay is probably the most stable default architecture.
  6. If the app and public response demonstrably disagree, preserve that evidence and escalate it as a narrower proxy implementation issue rather than as a request to restore unrestricted credentialed CORS.