Found in AI answers?Hire Theory Road
Theory Road.

Password-Protected Preview Images Broken? Check the Cache.

A password gate that answers image requests with a cacheable error can leave a phone showing broken images for as long as the cache header says. Here is how we find the stored error and clear it for good.

By October 18, 202610 min read

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.js or vercel.json matches the image path and attaches public, max-age=31536000, immutable to 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.

Request one image with no cookie.
Run curl against a single image URL from a clean shell. A 200 with an image content type means the gate is not touching assets and the problem is elsewhere. A 401, 403 or redirect means the gate runs on that path.
Read the matcher.
Open 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.
Find every rule that sets Cache-Control on that path.
Search 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.
Make every gate response uncacheable.
Wherever the gate returns a 401, 403 or redirect, set 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.
Serve the gate page's own assets publicly.
The password screen needs its logo, font and background before anyone has a cookie. Put those in a folder the matcher always skips, such as /gate/, or inline them.
Change the asset URLs once.
Devices that already stored the error will keep using it until it expires, and with a one-year header that is a long time. Rename the affected files or add a version query such as ?v=2 to every reference, then redeploy. New URLs mean new cache entries. Hashed Next.js static imports already do this.
Test on a device that has never visited.
Open the preview in a private tab on a phone, check images before and after the password, then recheck the phone that was broken.

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.

Broken images on a password-protected preview: symptom, cause and fix
SymptomLikely causeFix
Images broken on every device until login, fine afterGate runs on image pathsExclude assets in the matcher
Images still broken on one phone after loginStored 401 or redirect with long max-ageno-store on gate responses, then new asset URLs
Desktop fine, phone broken, same accountPhone requested images before it had the cookieChange asset URLs; test in a private tab
Password page shows a broken logoGate page loads a gated assetServe gate assets from an excluded path or inline them
curl shows 401 with immutablePath header rule applies to error responsesRemove the rule or limit it to hashed files
CSS or JS missing before loginMatcher includes _next/staticUse the documented negative lookahead
Purging the CDN changes nothingError is in the browser cache, not the edgeNew URLs or clear site data on the device
Login loops back to the gate on some pagesCached redirect to the login pageno-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/static with public, max-age=31536000, immutable., set by Next.js and not overridable
  • Normal: files in public/ with public, 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-store and no long lifetime
  • Red flag: any 401, 403, 302 or 307 with a max-age above 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 sets Cache-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.

Work with us

Let’s talk about what’s next.

A short note on where the business is and where it needs to go. A senior partner replies within one business day.

t@theoryroad.com

Step 1 of 2

How can we help you get found?

What do you need help with?

Next: name, email and budget. That’s it.