A staging site sits behind a session-cookie login. The page itself loads fine, but the request the browser makes for the URL in `<link rel="manifest">` comes back as the login redirect instead of the manifest JSON. What about that link explains it, and what is the fix?
answer
- the document was authenticated, the fetch was not
- same-origin does not imply cookies here
- one attribute, and the value matters
- anonymous is the default in disguise
- or just let the file out of the gate
basics
~20 sThe manifest is fetched without credentials by default, so the session cookie is not sent and the server answers with the login flow. Adding crossorigin="use-credentials" to the manifest link makes the browser send credentials — this applies even when the manifest is same-origin.
solid answer
~50 sA manifest is not fetched like a normal same-origin subresource: the browser requests it in a no-credentials mode by default, so cookies do not ride along. Behind a cookie-gated staging environment the server sees an unauthenticated request and answers with a 401 or a redirect to the login page, and the browser discards the response as an invalid manifest — while the document itself, which *was* credentialed, renders normally. The fix is `<link rel="manifest" href="/manifest.webmanifest" crossorigin="use-credentials">`, which switches the fetch to include credentials. The surprise is that this is needed even though the manifest sits on the same origin as the page. The alternative fix is to exempt the manifest path from the auth gate, which is usually fine since a manifest carries no secrets. If the manifest is on a different origin, `use-credentials` additionally requires the response to carry the right CORS headers.
code
html · 1 line<link rel="manifest" href="/manifest.webmanifest" crossorigin="use-credentials">go deeper
Know that the manifest is a separate HTTP request from the page, so it can fail on its own even when the page loads fine.
Be able to say that the manifest fetch omits credentials by default and that crossorigin="use-credentials" is the opt-in — and that anonymous is not a fix.
Show the diagnosis: read the actual response body rather than the status alone, distinguish an auth challenge from a MIME-type rejection, and weigh the markup fix against simply exempting the path from the gate.
Decide the policy — whether a staging gate should cover public-identity assets at all, and make the call consistently so staging and production ship identical markup rather than an environment-specific head.
## The symptom Everything about the page works. The document is authenticated, images load, the API works. Only the manifest is broken: the network panel shows the request to `/manifest.webmanifest` returning a 401, or a 302 to `/login` followed by an HTML response, and the browser reports that the site has no usable manifest. The instinct is to blame the path, the MIME type, or a build step that failed to emit the file. All three are normal causes of a dead manifest — but here the file is plainly being requested and plainly being answered, just with the wrong thing. ## Why the credentials do not travel Different subresource types are fetched with different defaults. A same-origin `<img>` or stylesheet request carries cookies as a matter of course. The manifest does not: it is fetched in a mode that omits credentials unless the author opts in. That default is deliberate. A manifest describes an application's public identity, and browsers avoid attaching a user's credentials to a fetch that can happen outside the normal page context. The consequence for a cookie-gated environment is direct: the server receives an anonymous request for a protected path and does what it does to every anonymous request — challenges it. The crucial and counter-intuitive part is that **this is not a cross-origin rule**. The credentials mode applies to the manifest fetch regardless of whether the manifest lives on the page's own origin. Engineers who reason "it's same-origin, so cookies obviously go" get stuck here for a long time. ## The opt-in ```html <link rel="manifest" href="/manifest.webmanifest" crossorigin="use-credentials"> ``` The `crossorigin` attribute on the manifest link is what changes the fetch mode. With `use-credentials`, the request carries cookies and HTTP authentication, the auth gate lets it through, and the JSON arrives. The attribute has two other states, and knowing them keeps the answer precise: - **absent** — the default no-credentials fetch, which is exactly the behaviour causing the bug. - **`crossorigin` or `crossorigin="anonymous"`** — the same no-credentials behaviour stated explicitly. Writing this does *not* fix the problem, which is a common wrong turn: people add the attribute, keep the default value, and conclude the fix does not work. ## Cross-origin manifests If the manifest genuinely lives on another origin — a shared CDN, say — then `use-credentials` is not the whole story. The response must also carry CORS headers that permit a credentialed read: an `Access-Control-Allow-Origin` naming the page's exact origin (a wildcard is not accepted for credentialed requests) together with `Access-Control-Allow-Credentials: true`. Getting one and not the other produces a fetch the browser makes and then refuses to hand over. For a same-origin manifest none of that applies; the attribute alone is enough. ## The other fix, and which to choose The second option is to leave the markup alone and exempt the manifest path from the authentication gate — allow `/manifest.webmanifest` (and typically the icon assets it references) through unauthenticated. Which is right depends on why the gate exists: - **A staging password wall** whose purpose is keeping an unfinished product out of search results and casual view: exempting a JSON file that lists a name, colours and icon URLs leaks nothing meaningful. This is usually the cleaner fix, because it also makes the icons load and keeps production and staging markup identical. - **A genuinely private application** where even the app's existence and branding are sensitive: use `crossorigin="use-credentials"` and keep the file behind the gate. A detail worth stating out loud: the icons referenced *from* the manifest are separate fetches with their own rules. Fixing the manifest fetch and leaving the icon paths behind the gate gets you a manifest the browser accepts and an icon set it cannot load. ## How to diagnose it in general When a manifest is fetched but ignored, walk four checks in order: 1. **Status and body** — is the response actually the JSON, or a login page / error page wearing a 200? 2. **Credentials** — is the path protected, and is the request going out anonymously? This is the case above. 3. **MIME type** — is it served as `application/manifest+json`? A manifest served as `text/html` or `application/octet-stream` gets discarded. 4. **Content** — is the JSON parseable, and do its relative URLs resolve where you think they do? The first two are indistinguishable in a screenshot of the head; only the network response tells you which one you have.
- Does adding `crossorigin="anonymous"` fix this instead?No — `anonymous` is the same no-credentials behaviour the browser already used, just written explicitly. It changes nothing about the failing request. Only `use-credentials` switches the fetch to send cookies, which is why people who add the attribute with the wrong value conclude the fix does not work.
- If you move the manifest to a shared CDN origin and keep `use-credentials`, what else has to be true?The CDN response must permit a credentialed cross-origin read: `Access-Control-Allow-Origin` echoing the page's exact origin — a wildcard is rejected for credentialed requests — plus `Access-Control-Allow-Credentials: true`. Without both, the browser makes the request and then refuses to expose the response, so the manifest is still ignored.
- You exempt the manifest path from the auth gate and the manifest now loads, but the installed icon is still blank. What did you miss?The icons listed in the manifest are separate fetches against their own URLs, with the same gate in front of them. Exempting only the JSON gets you a manifest the browser accepts and icon requests that still bounce off the login. Exempt the icon paths too, or serve them from an unauthenticated static path.
saying these in an interview costs you the question
- Assumes a same-origin manifest fetch automatically carries cookies
- Adds crossorigin="anonymous" and expects credentials to be sent
- Blames the MIME type without reading the actual response body
- Thinks CORS headers are needed for a same-origin manifest
- Fixes the manifest fetch and forgets the icon URLs behind the same gate