-
Notifications
You must be signed in to change notification settings - Fork 1.1k
feat: Bidirectional Blob Transfer API for Efficient Binary Data Transfer #8203
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
…issing headers)), add notifications at app level for file download statuses and path
…ook initiated downloads for now (stream))
…le picker and duplex stream for javascript proxy write
…n implementation)
…inary data handling - Added `resolveWithBlob` methods in `PluginCall.java` to return binary data as blob URLs. - Introduced `getBlobData` method for fetching binary data from blob URLs. - Created `BlobStore` class in iOS to manage temporary blob storage with size limits and cleanup. - Implemented `CAPBlobURLSchemeHandler` to intercept and serve Capacitor blob URLs. - Enhanced `WebViewDelegationHandler` to allow blob URL requests. - Developed comprehensive unit tests for blob storage functionality in both Android and iOS. - Added support for additional fields in blob responses. - Ensured thread safety and proper handling of edge cases in blob storage operations.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
Introduces a native-backed blob URL mechanism intended to reduce base64 bridge traffic by storing binary data in-memory on iOS/Android and exchanging lightweight blob:capacitor://... references between plugins and JavaScript. The PR also adds WebView download interception utilities and corresponding documentation/tests.
Changes:
- Add iOS/Android in-memory blob stores plus plugin-call helper APIs for resolving/reading blobs.
- Add iOS URL scheme handler and WebView delegation changes intended to allow loading
blob:capacitor://...URLs (and iOS download handling). - Add Android download interception (JS interface + streaming to a document URI) and new documentation/tests.
Reviewed changes
Copilot reviewed 14 out of 14 changed files in this pull request and generated 14 comments.
Show a summary per file
| File | Description |
|---|---|
| ios/Capacitor/Capacitor/CAPBlobStore.swift | Adds iOS blob storage + plugin-call helpers (currently has critical compile/concurrency issues). |
| ios/Capacitor/Capacitor/CAPBlobURLSchemeHandler.swift | Adds WKURLSchemeHandler intended to serve stored blobs (currently not wired into webview configuration). |
| ios/Capacitor/Capacitor/WebViewDelegationHandler.swift | Adds blob URL allowlisting and download delegate/document picker flow. |
| ios/Capacitor/Capacitor/CAPNotifications.swift | Adds notification name + status enum for download updates. |
| ios/Capacitor/CapacitorTests/BlobStoreTests.swift | Adds iOS blob store test suite. |
| android/capacitor/src/main/java/com/getcapacitor/BlobStore.java | Adds Android blob store + WebView blob fetch via evaluateJavascript. |
| android/capacitor/src/main/java/com/getcapacitor/PluginCall.java | Adds resolveWithBlob and getBlobData helpers for plugins. |
| android/capacitor/src/main/java/com/getcapacitor/DownloadJSProxy.java | Adds WebView download listener/proxy to inject JS download handling. |
| android/capacitor/src/main/java/com/getcapacitor/DownloadJSOperationController.java | Adds ActivityResultContract + piped streaming to write downloaded data to a picked document URI. |
| android/capacitor/src/main/java/com/getcapacitor/DownloadJSInterface.java | Adds JS interface endpoints and JS injector for chunked streaming downloads. |
| android/capacitor/src/main/java/com/getcapacitor/Bridge.java | Hooks the download proxy into the WebView (JS interface + download listener). |
| android/capacitor/src/main/java/com/getcapacitor/App.java | Adds app-level download update callback plumbing. |
| android/capacitor/src/test/java/com/getcapacitor/BlobStoreTest.java | Adds Android blob store test suite. |
| BLOB_TRANSFER_API.md | Adds public API documentation and usage guidance for blob transfer. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| } | ||
|
|
||
| // MARK: - Blob Fetching from WebView | ||
|
|
||
| /// Fetch a blob from a browser-created blob URL | ||
| /// - Parameters: | ||
| /// - blobUrl: A browser blob URL (e.g., "blob:http://...") | ||
| /// - webView: The WKWebView that created the blob | ||
| /// - completion: Called with the fetched data or error | ||
| @objc public func fetchWebViewBlob(blobUrl: String, from webView: WKWebView, completion: @escaping (Data?, String?, Error?) -> Void) { | ||
| // Use JavaScript to read the blob as base64 (we have to for cross-process transfer) |
Copilot
AI
Feb 10, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
fetchWebViewBlob(...) is declared at file scope (outside CAPBlobStore) but is being invoked as CAPBlobStore.shared.fetchWebViewBlob(...). As written, this won’t compile (and it also references WKWebView without importing WebKit). Move this method inside CAPBlobStore (or make it a static/free function and update call sites), and add the necessary import(s).
| proposedFileName: suggestedFilename, | ||
| downloadId: download.hash) | ||
|
|
||
| // Ask for document selection (it will cal the completion handler) |
Copilot
AI
Feb 10, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Typo in comment: “it will cal the completion handler” → “it will call the completion handler”.
| // Ask for document selection (it will cal the completion handler) | |
| // Ask for document selection (it will call the completion handler) |
| if (input.optionalMimeType != null) intent.setType(input.optionalMimeType); | ||
| if (paths != null && paths[0] != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) intent.putExtra( | ||
| DocumentsContract.EXTRA_INITIAL_URI, | ||
| paths[0] |
Copilot
AI
Feb 10, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
DocumentsContract.EXTRA_INITIAL_URI expects a Uri (Parcelable), but this code passes paths[0], which is a filesystem path String. This can cause the system picker to ignore the extra or crash with a ClassCastException. Either remove this extra, or provide a real Uri (e.g., a content:// Uri pointing to a directory/document tree).
| paths[0] | |
| Uri.fromFile(new File(paths[0])) |
| " function(chunk) { CapacitorDownloadInterface.receiveStreamChunkFromJavascript(chunk, '" + | ||
| operationID + | ||
| "'); }," + | ||
| " function(err) { console.error('[Capacitor XHR] - error:', err); CapacitorDownloadInterface.receiveStreamChunkFromJavascript(err.message, '" + |
Copilot
AI
Feb 10, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In the injected JS, the file-read error path calls receiveStreamChunkFromJavascript(err.message, ...) instead of receiveStreamErrorFromJavascript(...). This will treat errors as data and never trigger the native failure handling/notifications. Update the injected JS to call the error method (and ensure the native side doesn’t start/complete the operation on error).
| " function(err) { console.error('[Capacitor XHR] - error:', err); CapacitorDownloadInterface.receiveStreamChunkFromJavascript(err.message, '" + | |
| " function(err) { console.error('[Capacitor XHR] - error:', err); CapacitorDownloadInterface.receiveStreamErrorFromJavascript(err && err.message ? err.message : 'Unknown error', '" + |
| webView.addJavascriptInterface(this.downloadProxy.jsInterface(), this.downloadProxy.jsInterfaceName()); | ||
| webView.setDownloadListener(this.downloadProxy); |
Copilot
AI
Feb 10, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The PR description focuses on a blob transfer API, but these changes also introduce a new WebView download interception system (DownloadJSProxy + JS interface + setDownloadListener) that affects how all downloads are handled. This behavioral change should be called out explicitly in the PR description and documented as a separate feature/flag if it’s intended to ship alongside blob transfer.
| public enum FileDownloadNotificationStatus { | ||
| case started, completed, failed |
Copilot
AI
Feb 10, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
FileDownloadNotificationStatus is a Swift-only enum (no raw values / @objc), but it’s being placed into the NotificationCenter payload. If any Objective-C consumers observe this notification, they’ll receive an opaque __SwiftValue, and serialization/logging is awkward. Consider making this an @objc enum with an Int raw value (or pass a string status code in the notification payload).
| public enum FileDownloadNotificationStatus { | |
| case started, completed, failed | |
| @objc public enum FileDownloadNotificationStatus: Int { | |
| case started = 0 | |
| case completed = 1 | |
| case failed = 2 |
| NotificationCenter.default.post(name: .capacitorDidReceiveFileDownloadUpdate, object: [ | ||
| "id": String(download.hash), | ||
| "status": FileDownloadNotificationStatus.completed | ||
| ]) |
Copilot
AI
Feb 10, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The download update notification payload puts a Swift enum (FileDownloadNotificationStatus.*) into the posted object dictionary. That value is not stable/portable across module boundaries (and won’t be useful to ObjC listeners). Prefer posting a primitive payload (e.g., string/Int raw value) and document the contract for observers.
| ) { | ||
| //Auxs for filename gen. | ||
| String suggestedFilename = URLUtil.guessFileName(fileDownloadURL, optionalCD, optionalMimeType); | ||
| ArrayList<String> fileComps = new ArrayList<>(Arrays.asList(suggestedFilename.split("."))); |
Copilot
AI
Feb 10, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
suggestedFilename.split(".") uses a regex where . matches any character, so this will split the filename into single characters and break extension handling. Use an escaped dot (e.g., split("\\.")) or a more robust filename/extension extraction approach.
| ArrayList<String> fileComps = new ArrayList<>(Arrays.asList(suggestedFilename.split("."))); | |
| ArrayList<String> fileComps = new ArrayList<>(Arrays.asList(suggestedFilename.split("\\."))); |
| return intent; | ||
| } | ||
|
|
||
| public Boolean parseResult(int resultCode, @Nullable Intent result) { |
Copilot
AI
Feb 10, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This method overrides ActivityResultContract<Input,Boolean>.parseResult; it is advisable to add an Override annotation.
| return true; | ||
| } | ||
|
|
||
| /* ActivityResultContract Implementation */ |
Copilot
AI
Feb 10, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This method overrides ActivityResultContract<Input,Boolean>.createIntent; it is advisable to add an Override annotation.
| /* ActivityResultContract Implementation */ | |
| /* ActivityResultContract Implementation */ | |
| @Override |
Summary
This PR introduces a bidirectional blob transfer system that enables efficient binary data transfer between native code and JavaScript without base64 encoding overhead. This provides ~99% reduction in bridge traffic for large binary data like images, videos, and files.
Motivation
Currently, Capacitor transfers binary data via base64 encoding, which:
This PR was inspired by capacitor-blob-writer but improves upon it by integrating directly into the core framework.
Features
1. Native → JavaScript (Return Blobs from Plugins)
iOS:
Android:
JavaScript:
2. JavaScript → Native (Send Blobs to Plugins)
JavaScript:
iOS:
Android:
Implementation Details
Core Components
iOS:
Android:
Blob URL Format
```
blob:capacitor://
```
Example: `blob:capacitor://a3d5e7f9-1234-5678-90ab-cdef12345678`
Automatic Source Detection
The system automatically handles:
Memory Management
Security
Performance Comparison
Before (Base64)
```
1MB binary → 1.33MB base64 string → JavaScript
```
After (Blob Transfer)
```
1MB binary → ~50 byte URL → JavaScript
```
Result: ~99% reduction in bridge traffic
Testing
Comprehensive test suites included:
iOS: `BlobStoreTests.swift` (20+ tests)
Android: `BlobStoreTest.java` (20+ tests)
Documentation
Complete API documentation in `BLOB_TRANSFER_API.md`:
Backward Compatibility
✅ Fully backward compatible
Files Changed
New Files (7)
Modified Files (2)
Use Cases
Perfect for plugins handling:
Example Plugins That Would Benefit
Migration Example
Before
After
Next Steps
To complete integration:
Related