When images are broken on a password-protected preview site, and stay broken on a phone even after the visitor logs in, the cause is almost always one of six things: the auth gate runs on image and asset paths, the gate answers those requests with a 401 or a redirect, a path rule stamps that error with a long max-age or immutable header, the image was requested before the auth cookie existed, the login page itself loads gated assets, or the asset URLs never changed after the fix so the phone keeps its stored error. Fix it by gating HTML pages only, marking every auth-dependent response Cache-Control: no-store, and changing the asset URLs once so devices fetch fresh copies. We ran into this on one of our own client preview sites. Desktop looked fine. The phone kept showing empty image boxes after login.
A password-protected preview is a staging copy of a site behind a simple gate: middleware (called Proxy in Next.js 16) checks for an auth cookie on each request and, if it is missing, returns a 401 or redirects to a password page. The browser's HTTP cache stores responses by URL and reuses them until they go stale. If a gated image URL ever returns an error that carries a long freshness lifetime, the browser can keep serving that stored error for the same URL without asking the server again, cookie or not.
The six causes we find most often.
These stack. The stuck-on-phone symptom needs several at once.
- Gate runs on every path: the middleware or Proxy has no matcher, or a matcher that still includes
public/images, so image requests hit the password check like pages do - Error response for assets: an image request without the cookie gets a 401 or a redirect to the login page instead of image bytes, so the img tag renders as broken
- Long-lived cache header on the error: a header rule in
next.config.jsorvercel.jsonmatches the image path and attachespublic, max-age=31536000, immutableto whatever goes out, including the 401 - Image requested before the cookie: the phone opened the link, prefetched or previewed the page, or loaded the gate page's own background image before the visitor entered the password
- Login page loads gated assets: the password screen uses the site's logo or hero image from a gated path, so the very first image request is guaranteed to fail and be stored
- Same URL after the fix: the gate and headers were corrected, but the images still live at the exact URLs the phone already cached as errors, so nothing is requested again
Can a browser cache a 401 response?
Yes. A 401 is not cacheable by default, but any response with an explicit max-age or Expires header can be stored, whatever its status code. RFC 9110 lists the status codes that are heuristically cacheable (200, 203, 204, 206, 300, 301, 308, 404, 405, 410, 414 and 501), and 401 is not on it. RFC 9111, the HTTP caching standard, then lets a cache store a response that carries a public directive, a max-age directive or an Expires header. So a 401 with no cache headers is safe. A 401 that picks up max-age=31536000 from a path rule is a one-year instruction to keep the error.
The immutable directive makes it worse. MDN defines it as telling the cache that the response will not be updated while it is fresh, so the browser skips the conditional request it would otherwise send to check.
Does the CDN cache the 401 too?
On Vercel, no. Vercel's CDN only caches responses with status 200, 404, 410, 301, 302, 307 or 308, so a 401 stays out of the edge cache and lives only in the visitor's browser. That is why purging the CDN does nothing for this bug. The bad copy is on the device.
A redirect is different. A 307 to the login page is on Vercel's cacheable list, so with a long lifetime it can be cached at the edge as well as on the phone.
How to verify the cached error.
Check the headers the server sends, then check what the device actually stored. Three tools, in this order.
curl from a terminal, with no cookie. Run curl -sI https://your-preview-host/images/hero.jpg. Read the status line and the cache-control line. A 401 or 307 next to max-age=31536000 or immutable confirms the poison. Repeat with your gate cookie and confirm a 200.
Chrome DevTools on desktop. Open DevTools, go to the Network tab, filter to Img, and reload. The Status column shows 401 or 307 for gated images. The Size column shows (disk cache) or (memory cache) when the browser served a stored copy without a request. Tick Disable cache and reload: if images now load while logged in, the server is fine and the stored copy is the problem.
Safari Web Inspector for the phone. On the iPhone, turn on Web Inspector in Safari's Advanced settings, connect it to a Mac, and open the page from the Mac's Safari Develop menu under the device name. The Network tab shows the same status and cache columns for what the phone really did. It is the same setup we use for iOS Safari rendering bugs.
Diagnose top to bottom.
Each step isolates one layer. Stop at the first failure and fix it before moving on.
middleware.ts or proxy.ts and read export const config. Next.js documents that without a matcher, Proxy runs on every request, including _next/static, _next/image and files in public/. Use a negative lookahead that skips _next/static, _next/image, metadata files, and your image folder or common image extensions.next.config.js headers, vercel.json headers and the middleware itself. Next.js serves public/ files with Cache-Control: public, max-age=0 by default, so a one-year header on /images/ was added by someone. Remove it, or narrow it to files whose names change when their content does.Cache-Control: no-store explicitly on that response. MDN defines no-store as telling caches of any kind, private or shared, not to store the response. Add Vary: Cookie to gated HTML as a second guard, so a stored copy fetched without the cookie never matches a request that has it./gate/, or inline them.?v=2 to every reference, then redeploy. New URLs mean new cache entries. Hashed Next.js static imports already do this.Should the password gate cover images at all?
For a client preview, gate the HTML pages and let static assets through. The pages are what you are hiding. Image URLs are rarely found without the page that uses them. If the images themselves are confidential, such as unreleased product shots, keep them gated but return no-store on every denial.
A gate on HTML also keeps crawlers away from the preview content. When the site launches, follow a migration checklist so the gate and any noindex headers come off cleanly, then watch for new pages that are not getting indexed.
Is platform password protection better than custom middleware?
Platform-level protection takes your own matcher and header rules out of the gate, because the platform runs the password check, not your code. As of October 2026, Vercel Password Protection is available on Pro at $20 per month per protected project and is included on Enterprise. Visitors enter the password once per deployment, and Vercel sets a cookie for that deployment URL. Vercel documents that the cookie is valid only for the URL it was set on, so a password entered on one preview URL does not carry to another.
If you keep custom middleware, the matcher and the no-store rule above are the whole fix. Keep the shared password in an environment variable, never in the repo, for the reasons in our piece on secrets still in git history.
| Symptom | Likely cause | Fix |
|---|---|---|
| Images broken on every device until login, fine after | Gate runs on image paths | Exclude assets in the matcher |
| Images still broken on one phone after login | Stored 401 or redirect with long max-age | no-store on gate responses, then new asset URLs |
| Desktop fine, phone broken, same account | Phone requested images before it had the cookie | Change asset URLs; test in a private tab |
| Password page shows a broken logo | Gate page loads a gated asset | Serve gate assets from an excluded path or inline them |
| curl shows 401 with immutable | Path header rule applies to error responses | Remove the rule or limit it to hashed files |
| CSS or JS missing before login | Matcher includes _next/static | Use the documented negative lookahead |
| Purging the CDN changes nothing | Error is in the browser cache, not the edge | New URLs or clear site data on the device |
| Login loops back to the gate on some pages | Cached redirect to the login page | no-store on redirects; Vary: Cookie on HTML |
What is normal and what is a red flag?
A 401 or a redirect from a password gate is normal. A 401 or redirect that carries max-age, s-maxage, public or immutable is the red flag.
- Normal: hashed files under
/_next/staticwithpublic, max-age=31536000, immutable., set by Next.js and not overridable - Normal: files in
public/withpublic, max-age=0, the Next.js default, and a 200 with or without the cookie - Normal: gated HTML returning 401 or a redirect with
no-storeand no long lifetime - Red flag: any 401, 403, 302 or 307 with a
max-ageabove zero - Red flag: logged-in HTML marked
public, which shared caches are then allowed to store - Red flag: a header rule whose source pattern is
/:path*or/(.*)and setsCache-Control, because it touches every response the gate produces
The same rule applies to anything in front of a page, including a firewall challenge like the Vercel Security Checkpoint on your own site: check what goes over the wire, not the config screen.
Why are images broken on my phone but not my computer?
The phone most likely requested the images before it had the auth cookie, got a 401 or a redirect, and stored that error because a header rule gave it a long max-age. The computer either logged in first or never cached the error. Change the asset URLs so the phone fetches fresh copies.
Does clearing the cache fix broken images on a password-protected site?
Clearing the site's data on the affected device removes the stored error, so it fixes that one device. It does not fix anyone else. The durable fix is no-store on gate responses plus new asset URLs.
What Cache-Control header should a 401 response have?
Cache-Control: no-store. A 401 is not heuristically cacheable under RFC 9110, so a cache will not store it unless the response gives it explicit freshness or a public directive. no-store removes any doubt.
Which paths should Next.js middleware or Proxy skip on a gated site?
At minimum _next/static, _next/image, favicon.ico, robots.txt and sitemap.xml, as in the Next.js negative lookahead example, plus your public image folder and any assets the password page uses.
Is immutable safe to use on images?
Only when the URL changes whenever the file changes, such as a filename with a content hash. On a fixed path like /images/hero.jpg, immutable tells the browser not to check for updates while the response is fresh, so a replaced image or a stored error stays until it expires.