Converting a Blob URL into a Shareable File
This guide is part of Sharing Files and Media Payloads, which itself is part of Native Device Integration Patterns.
Blob URLs are everywhere in a modern editor: an image preview from URL.createObjectURL, a cropped result, a recorded audio clip, a generated document held in memory. All of them look like URLs, which makes it tempting to pass one to navigator.share({ url }). That never works — a blob: URL is scoped to the document that created it, so the recipient has nothing to fetch. The bytes have to travel, not the reference.
Feature Detection Gate
Validate with the actual file you are about to build, since support depends on its type.
// blob-share-support.js
export function canShareFile(file) {
if (window.isSecureContext !== true) return false;
if (typeof navigator.share !== 'function') return false;
if (typeof navigator.canShare !== 'function') return false;
if (!(file instanceof File)) return false;
try {
return navigator.canShare({ files: [file] });
} catch {
return false;
}
}
Step 1 — Fetch the Blob URL Back into Bytes
fetch resolves a blob: URL against the in-memory blob store, so this costs no network traffic — it is simply the cleanest way to read bytes you already hold.
// blob-url-to-file.js
export async function blobUrlToFile(blobUrl, name, type) {
const res = await fetch(blobUrl);
if (!res.ok) throw new Error(`blob URL is no longer valid: ${blobUrl}`);
const blob = await res.blob();
const resolvedType = type || blob.type || guessTypeFromName(name);
if (!resolvedType) throw new Error(`cannot determine a MIME type for ${name}`);
return new File([blob], name, { type: resolvedType, lastModified: Date.now() });
}
function guessTypeFromName(name = '') {
const ext = name.split('.').pop().toLowerCase();
return {
png: 'image/png', jpg: 'image/jpeg', jpeg: 'image/jpeg', webp: 'image/webp',
gif: 'image/gif', pdf: 'application/pdf', mp4: 'video/mp4', webm: 'video/webm',
mp3: 'audio/mpeg', wav: 'audio/wav', txt: 'text/plain', json: 'application/json'
}[ext] || '';
}
A failed fetch here has one overwhelmingly likely cause: the URL was already revoked. That is worth a distinct error message, because the alternative — a generic network failure — sends people looking at their connection.
Where you still hold the original Blob, skip the round trip entirely:
// prefer-the-blob.js
export function blobToFile(blob, name, type = blob.type) {
if (!type) throw new Error(`missing MIME type for ${name}`);
return new File([blob], name, { type, lastModified: Date.now() });
}
Keeping a reference to the Blob alongside the URL is the better architecture: the URL is for display, the blob is for sharing, and neither has to be reconstructed from the other.
Step 2 — Give the File a Name Users Will Recognise
Object URLs end in a UUID. If that becomes the filename, the recipient receives 9f2c1a8e-… with no extension and no way to open it.
// naming.js
export function shareName(base, type) {
const ext = {
'image/png': 'png', 'image/jpeg': 'jpg', 'image/webp': 'webp',
'application/pdf': 'pdf', 'video/mp4': 'mp4', 'audio/wav': 'wav'
}[type] || 'bin';
const safe = base
.replace(/[^\w\- ]+/g, '') // strip characters some filesystems reject
.trim()
.slice(0, 60) || 'shared-file';
return `${safe}.${ext}`;
}
Deriving the extension from the MIME type rather than from the original URL keeps the two consistent, which matters because some receiving apps dispatch on the extension and others on the declared type.
Step 3 — Share, Then Revoke
Preparation happens ahead of the gesture; the handler only shares.
// share-preview.js
import { blobUrlToFile } from './blob-url-to-file.js';
import { canShareFile } from './blob-share-support.js';
import { shareName } from './naming.js';
export function createPreviewSharer(blobUrl, { base = 'preview', type = 'image/png' } = {}) {
let file = null;
const prepare = async () => {
file = await blobUrlToFile(blobUrl, shareName(base, type), type);
};
async function share() {
if (!file) return { outcome: 'fallback', reason: 'not-ready' };
if (!canShareFile(file)) return { outcome: 'fallback', reason: 'unsupported' };
try {
await navigator.share({ files: [file], title: base });
return { outcome: 'shared' };
} catch (err) {
if (err.name === 'AbortError') return { outcome: 'dismissed' };
return { outcome: 'fallback', reason: err.name };
}
}
function dispose() {
URL.revokeObjectURL(blobUrl);
file = null;
}
return { prepare, share, dispose };
}
dispose is the part teams forget. Every unrevoked object URL pins its blob for the life of the document, so an editor that previews twenty photos holds all twenty in memory indefinitely — and on a phone that ends as a tab discard the user reads as a crash.
Failure Modes and Recovery
| Symptom | Cause | Minimal fix |
|---|---|---|
| Recipient gets an unopenable link | blob: URL passed as url |
Send bytes in files instead |
fetch on the blob URL fails |
URL already revoked | Keep the original Blob, or revoke later |
| File arrives named like a UUID | Object URL used as the filename | Derive a readable name plus extension |
canShare throws |
Blob passed where a File was required |
Wrap with the File constructor |
| Memory grows while browsing | Object URLs never revoked | revokeObjectURL when the preview is discarded |
| Type is empty on the receiving side | Relied on blob.type from a cached source |
Pass an explicit type to the constructor |
Browser and Platform Caveat
URL.createObjectURL, fetching a blob: URL, and the File constructor all behave consistently across Chrome, Safari, Firefox and Edge, so the conversion itself is portable; only the sharing step varies by platform. Two practical notes. Object URLs are bound to the document that created them, which means one minted inside a service worker or a different window is not usable in the page — pass the Blob across contexts instead, exactly as the share queue does when it stores payloads. And iOS Safari is the most aggressive about discarding memory-heavy tabs, so revocation discipline matters more there than anywhere else: an editor that leaks a few dozen full-resolution previews will be reloaded underneath the user mid-session.
FAQ
Can I pass a blob URL string to navigator.share as a url?
No. A blob: URL is meaningful only inside the document that created it, so a receiving app has nothing to resolve. Some browsers reject it outright and others share a link the recipient cannot open. Send the bytes as a File in the files array instead.
Does fetching a blob URL cost a network request?
No. A blob: URL resolves against the in-memory blob store, so fetch returns the existing bytes without touching the network. It is effectively a structured way to read a blob you already hold.
What happens if I forget URL.revokeObjectURL?
The blob stays alive for the lifetime of the document, so a gallery or editor that mints a URL per item retains every byte the user has viewed. Memory grows steadily and, on a phone, the tab is eventually discarded — which looks like a random crash rather than a leak.