DEV Community

Timur
Timur

Posted on

The customer said the page was blank. The page was fine. The tab was poisoned.

A paying customer clicked the link in their confirmation email and got an infinite loader. We opened the same link: everything worked. Their console had no errors. Reload didn't help them. Hard reload didn't help. The page was, as far as our monitoring could tell, perfectly healthy.

It took an embarrassing amount of time to figure out that nothing was wrong with the page. Something was wrong with the tab.

Quick context: I build Hitou, a small product that writes and produces a personalized song about someone you love. Almost everything a customer touches after checkout arrives by email — the track page, the gift page, the upload form. Which means almost everything a customer touches gets opened from inside an email client. That detail turned out to be the entire story.

The mechanism: sandbox inheritance

HTML email gets rendered inside sandboxed iframes all over the place: webmail clients, corporate mail portals, embedded previews in support tools. The sandbox attribute on those iframes strips capabilities from the content — and scripts don't run unless allow-scripts is explicitly granted.

Here's the part that isn't in most people's mental model. If the sandbox includes allow-popups but not allow-popups-to-escape-sandbox, then a link with target="_blank" does open a real, full-size browser tab — and that tab inherits every sandbox restriction of the iframe it came from, including the script ban.

What that tab looks like from the inside:

  • The HTML and every static asset load fine. Status 200, every byte delivered, network tab looks perfect.
  • No JavaScript executes. Not "throws an error" — never starts.
  • The console is clean, because errors come from scripts, and no script ran.
  • Reload and hard-reload change nothing. The sandbox belongs to the tab, not to the document. The only cure is a fresh tab the user opens by hand.

Now combine that with a modern frontend habit: pages that start with their content hidden and reveal it with JS after fetching state. In a sandbox-inherited tab, that page renders as — nothing. A header and a void. Or, in our case, an eternal loader that no amount of waiting would ever resolve.

The user is not doing anything exotic. They clicked a link in an email. That's the least exotic action on the internet.

Why your monitoring will never see it

This failure is invisible from the server side. The request arrives with normal headers, gets a 200, downloads all assets. Synthetic checks pass because your uptime bot isn't running inside a sandbox. Error trackers stay silent because error trackers are JavaScript, and JavaScript is exactly what's banned.

The only signal is a human writing to support: "the page is just blank." And the natural first response — "try refreshing" — is precisely the advice that cannot work.

The three-layer fallback

The fix isn't to escape the sandbox — you can't, from the inside. The fix is to make the page degrade honestly instead of lying with a loader. We ended up with three layers, and each exists because of a specific gotcha.

Layer 1: a <noscript> note — with no links in it.

<noscript><p class="nojs-note">Having trouble? This window can't run the page.
  Copy the link from the address bar and open it in a new browser tab -
  the upload form will load right away.</p></noscript>
Enter fullscreen mode Exit fullscreen mode

The tempting version of this note contains a helpful <a target="_blank"> to the same URL. Don't. Inside an inherited sandbox, that link opens yet another sandboxed tab — the restriction propagates through every popup chain. Only a tab the user opens by hand escapes. So the copy says: copy the address, open it yourself. Less elegant, actually works.

Layer 2: a CSS-only delayed banner, for a different failure.

<noscript> only fires when scripting is disabled. There's a sibling failure where scripting is allowed but your JS never finishes — a bundle that never loaded, a script blocked by an extension, a top-level throw. For that we use a banner that reveals itself by pure CSS animation after 10 seconds:

.js-fallback {
  max-height: 0; opacity: 0; overflow: hidden;
  animation: js-fallback-reveal .3s ease 10s forwards;
}
.js-alive .js-fallback { display: none !important; animation: none; }
@keyframes js-fallback-reveal { to { max-height: 200px; opacity: 1; } }
Enter fullscreen mode Exit fullscreen mode

The base state is collapsed (max-height: 0), not display: none — no reserved box, no layout shift while invisible, and no dependency on JS to reveal it. If the page's script is healthy, it disarms the banner by adding a class:

// the LAST line of the page's IIFE
document.documentElement.classList.add("js-alive");
Enter fullscreen mode Exit fullscreen mode

Last line, deliberately: any top-level throw before it leaves the banner armed. The banner's job is "JS loaded but died," and a script that died can't un-arm it.

Layer 3: make sure layers 1 and 2 never stack.

With scripting off, the CSS animation still runs — so a sandboxed tab would eventually show both messages. One line in the head hands ownership to <noscript>:

<noscript><style>.js-fallback{display:none!important}</style></noscript>
Enter fullscreen mode Exit fullscreen mode

The relapse, which is the actual lesson

We shipped all of this after the first incident, across every page the app had. Then we built a new page — an upload form for video testimonials. Its content starts hidden; JS reveals the right card. And it's reached exclusively from links in emails.

It launched without the fallbacks. Of course it did: the protection lived in the old templates, and the new page was new. The one page whose entire traffic comes from the highest-risk context was the one page with no defense. A customer landed in a sandboxed tab, saw a blank void, and we got to have this discovery a second time.

No automated test we could write flags this class of miss, because a test can't know which pages will be linked from emails — that's a fact about your email templates and your future marketing, not about the code. What works is a checklist question at page-creation time:

  1. Will emails ever link to this page?
  2. Does the page start with hidden content that JS reveals?

Two yeses → the three layers go in before launch. (Baking the layers into a base layout would cover it too — but the noscript copy is page-specific; "the upload form will load right away" means nothing on a gift page.) It's a two-minute copy-paste that closes a failure mode that is otherwise invisible until a paying customer meets it.

Takeaways

  • A tab spawned from a sandboxed iframe without allow-popups-to-escape-sandbox inherits the sandbox. Full-size, real-looking tab; no JavaScript; clean console; reload-proof.
  • "Assets load, console is silent" doesn't mean the page works. Console silence has two very different causes: nothing went wrong, or nothing ran.
  • Never put links in a <noscript> rescue note — popups inherit the sandbox too. Tell the user to copy the address into a fresh tab.
  • Arm your JS-death fallback with pure CSS and disarm it with the last line of your script. The order matters: a fallback that needs JS to appear protects you from nothing.
  • Pages linked from emails are their own risk surface. Track them as such, or a future page will relearn this the way we did.

Earlier field notes from the same build, on how AI APIs fail behind 200 OK: 200 OK, content: null.

Top comments (0)