How Safari Handles Dynamic Blob and Data URL Downloads

This article provides an overview of how the Safari web browser processes and manages file downloads initiated dynamically through client-side JavaScript using blob: and data: URLs. It covers how Safari's WebKit engine interacts with the HTML5 download attribute, the specific behaviors and limitations of both URL schemes, security constraints like user gesture requirements, and the practical workarounds developers use to ensure reliable file downloads across macOS and iOS devices.

The Role of the HTML5 Download Attribute

To trigger a client-side download in modern browsers, developers dynamically create an HTML anchor (<a>) element, assign the blob: or data: URL to its href attribute, set the download attribute to the desired filename, and programmatically trigger a click event.

Safari supports the download attribute starting from Safari 10.1 on macOS and iOS 13 on iOS/iPadOS. Without this attribute, Safari attempts to navigate to the URL and render the content inline rather than downloading it as a standalone file.

Handling blob: URLs

A blob: URL (Binary Large Object) is generated in memory using the URL.createObjectURL() method. It is the preferred method for downloading larger files generated on the client side. Safari handles blob: URLs with a few specific caveats:

  • MIME Type Sensitivity: Safari relies heavily on the Blob object's MIME type. If you specify a known MIME type that Safari can render natively (such as application/pdf, image/png, or text/plain), Safari may occasionally ignore the download attribute and open the file in the current tab—especially if the download is not triggered correctly. Setting the MIME type to application/octet-stream forces Safari to treat the blob as a generic binary file, ensuring a download occurs, though it may sometimes cause Safari to append an incorrect file extension if the download attribute is malformed.
  • Premature URL Revocation: To free up memory, developers call URL.revokeObjectURL() after triggering the download. In Chrome and Firefox, this can be done immediately after the programmatic .click() call. In Safari, revoking the URL immediately causes the download to fail silently because WebKit handles the download asynchronously. Developers must wrap URL.revokeObjectURL() in a setTimeout (typically 100ms to 1000ms, or up to 40 seconds for very large files) to give Safari enough time to initiate the file stream.
  • Cross-Origin Restrictions: blob: URLs are bound to the origin of the document that created them. Safari strictly enforces same-origin policies, meaning a blob: URL generated in an iframe from a different domain cannot be downloaded using the download attribute in the parent frame.

Handling data: URLs

A data: URL embeds the file content directly into the URL string, usually encoded in Base64. While useful for small files (like small SVGs, JSON snippets, or canvas exports), Safari enforces strict security and performance limits on them:

  • Top-Level Navigation Blocking: As a security measure against phishing, WebKit blocks top-level navigation to data: URLs. If a script attempts to trigger a download by calling window.open('data:...') or setting window.location.href = 'data:...', Safari will block the action and log a security error in the console.
  • Mandatory Anchor Tag Usage: Because top-level navigation is blocked, data: URLs must be downloaded using an <a download="filename.ext"> element. When combined with the download attribute, Safari bypasses the navigation block and routes the data stream directly to the browser's download manager.
  • Size Limitations: Because data: URLs store the entire file as a string in memory, they consume significantly more RAM than blobs. Safari on iOS has strict memory limits for web views; attempting to generate and download a data: URL larger than a few megabytes can crash the browser tab.

The User Gesture Requirement

Safari features aggressive popup and download blockers to protect user experience. A dynamic download using a blob: or data: URL will only succeed if it is triggered by a "trusted" user gesture, such as a direct mouse click or tap event.

If your JavaScript application needs to fetch data from an API or perform a lengthy asynchronous task (like generating a complex PDF) before creating the blob: or data: URL, the trusted event context expires while waiting for the Promise to resolve. When the programmatic anchor.click() finally executes, Safari will block the download.

To bypass this limitation in Safari, developers must separate the asynchronous generation step from the download step. For example, a user clicks a "Generate File" button (which prepares the blob asynchronously), and once ready, the UI displays a "Download File" button. Clicking this second button executes the blob: download synchronously within a fresh user gesture context.

Differences Between macOS and iOS Safari

While both platforms use WebKit, the user experience differs slightly:

  • macOS Safari: Downloads initiated via blob: or data: URLs go straight to the user's designated "Downloads" folder, flying into the download manager icon in the browser toolbar without interrupting the user, provided the site has permission to download files.
  • iOS Safari: When a blob: or data: download is triggered on an iPhone or iPad, iOS Safari always interrupts the user with a system modal asking, "Do you want to download '[filename]'?" The user must manually tap "Download" for the file to be saved to the iOS Files app. If the blob: URL was revoked before the user taps this confirmation button, the download will fail. Therefore, on iOS, it is often safer not to revoke the blob: URL immediately, or to delay revocation for at least 30 to 60 seconds.