Blob URLs Don't Do Relative Paths (And Other iframe Surprises)
Building a system that renders interactive HTML artifacts inside iframes sounds straightforward. You generate some HTML, stuff it in a <blob> URL, point an iframe at it, done. Except the browser has a lot of opinions about what iframes can and can’t do, and several of those opinions aren’t written anywhere obvious. Here are two spec edge cases that cost me a debugging session each.
Bug 1: fetch() Fails on Relative Paths Inside a Blob URL Iframe¶
The setup: an artifact — a rich interactive HTML widget — is rendered inside an iframe. The artifact’s JavaScript calls fetch('/media/artifacts/eir.html') to load some data. Straightforward relative path, right? Works fine everywhere else.
The error:
Failed to parse URL from /media/artifacts/eir.html
TypeError: Failed to parse URL from /media/artifacts/eir.html
at new Request (...)
That error message is cryptic. “Failed to parse a URL”? /media/artifacts/eir.html is a perfectly valid URL path. It parses fine in any other context.
Except it doesn’t parse fine here, and the reason is in the browser spec.
What’s actually happening: When you create a blob URL (blob:https://yourserver.com/some-uuid) and use it as an iframe’s src, the iframe’s document base URL is that blob URL. When fetch() encounters a relative path, it tries to resolve it against the document’s base URL. The base URL is blob:https://yourserver.com/some-uuid. The browser then tries to construct an absolute URL from that blob scheme origin — and it can’t. Blob URLs are opaque; they don’t have a real origin in the way https:// URLs do for the purpose of relative path resolution.
The spec is technically correct: blob URL documents don’t have a browsing context origin that makes relative paths meaningful. But the error message doesn’t tell you any of that. It just says it failed to parse a URL that looks totally parseable.
The fix: Inject a <base href> tag into every blob URL document you create:
const origin = window.location.origin; // e.g. https://yourserver.com
const baseTag = `<base href="${origin}/">`;
// Inject at the top of <head>, or prepend if no <head>
if (html.includes('<head>')) {
html = html.replace('<head>', `<head>\n ${baseTag}`);
} else {
html = baseTag + html;
}
const blob = new Blob([html], { type: 'text/html' });
const blobUrl = URL.createObjectURL(blob);
The <base href> tag overrides the document’s base URL. Now relative paths resolve against your server origin instead of the blob scheme. fetch('/media/artifacts/eir.html') becomes fetch('https://yourserver.com/media/artifacts/eir.html') and the world makes sense again.
Worth noting: you need to do this injection at every point where you create a blob URL from artifact HTML — any wrapper code, any modal code, any secondary renderer. If you miss one, you get a silent failure that only shows up in specific user flows.
Bug 2: Setting srcdoc="" Poisons the iframe¶
Different bug, same system, different part of the spec.
The symptom: pop an artifact out into a modal, go fullscreen, close it, pop it back out, go fullscreen again — blank page. Not an error, just nothing. The artifact renders fine the first time through. Second fullscreen cycle: white void.
The culprit was in the cleanup code:
function closeArtifactModal() {
// ... hide the modal ...
frame.srcdoc = ''; // "clear" the iframe
}
This looks like reasonable cleanup. You’re closing the modal, you want the iframe empty, you set srcdoc to an empty string. What could go wrong?
What goes wrong is that srcdoc and src are two different content mechanisms for iframes, and the HTML spec has explicit priority rules: if srcdoc is present as an attribute (even empty string), it takes precedence over src.
Setting frame.srcdoc = '' doesn’t clear the iframe. It sets the srcdoc attribute to an empty string, which means the browser renders an empty document — and continues to ignore whatever you put in frame.src. Every subsequent frame.src = newBlobUrl call is silently dropped. The frame stays blank forever.
The fix: Remove the attribute entirely instead of setting it to empty:
function closeArtifactModal() {
// ...
frame.removeAttribute('srcdoc'); // ✓ — removes the attribute, src takes over
// NOT: frame.srcdoc = ''; // ✗ — poisons the iframe
}
Or if you’re using JavaScript to set the content initially, don’t touch srcdoc in cleanup at all — just reset frame.src:
frame.src = 'about:blank'; // navigates to blank page, doesn't set srcdoc
The lesson here is that “empty string” and “attribute not present” are genuinely different states for HTML attributes, and for srcdoc specifically that difference has significant behavioral consequences. Setting an attribute to '' is not the same as removing it.
The Common Thread¶
Both bugs share a pattern: they look like they should work, the error messages don’t explain why they don’t, and the real explanation requires knowing a specific corner of the HTML/fetch spec that you’d only look up after you’ve already hit the wall.
The blob URL base resolution issue is documented in the Fetch spec and the HTML Living Standard, but not in any “common iframe pitfalls” listicle I’ve ever seen. The srcdoc precedence behavior is in the HTML spec too — the attribute takes precedence over src regardless of its value — but it’s the kind of thing you only commit to memory after it bites you.
If you’re building anything that renders HTML inside iframes — artifact systems, sandboxed editors, preview panels, document viewers — the spec surface area is larger than it looks. A few heuristics that would have saved me time:
- Relative paths inside blob URL documents need a
<base href>tag. Always inject it. It costs nothing and prevents a class of failures that produce confusing error messages. srcdocandsrcdon’t compose the way you’d expect. If you use both on the same element at different points in its lifecycle, you will eventually hit precedence behavior.- Cleanup code is where these bugs hide. Setting attributes to “empty” values is not the same as clearing state. Remove attributes. Reset to
about:blank. Be explicit about what state you want the iframe in.
Iframes are one of those web platform features with a 30-year history of edge cases layered on top of each other. The good news is that the spec is actually pretty readable once you know what to look for. The bad news is that you usually find out what to look for by losing a debugging session to a cryptic error about a URL that looks totally parseable.
Now it’s in the post. Maybe it saves you one of those sessions.