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:
- Compare the actual GET and POST/PATCH/PUT request configurations.
- Send the preflight manually.
- Check whether Express receives it.
- Compare credentialed and uncredentialed requests against a harmless probe route.
- 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:
- a Hub API
by-subdomain lookup returning a 400 error;
- 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:
- GET without credentials;
- GET with credentials;
- JSON POST without credentials;
- 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:
- Do not assume this is a temporary CORS outage that will restore browser-to-Space credentialed Cookies.
- Do not assume your case is identical until the GET and mutation request shapes are compared.
- If removing credentials makes the same POST/PATCH/PUT work, the new Spaces credential boundary is the leading explanation.
- If no credentials are involved, investigate ordinary preflight and the actual backend status.
- If Cookie authentication is essential, a same-origin Vercel BFF/server-side relay is probably the most stable default architecture.
- 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.