Automattic / Automattic/Gravatar-SDK-iOS

PhotosImagePicker leaks its continuation and silently drops the photo when image loading fails

Open
#812 0 comments 0 reactions 0 assignees View on GitHub
[Feature] Gravatar-UI [Priority] Medium Bug
Dominant language
Swift
Stars
141
Forks
44
Avg merge
5h 9m
Merged PRs (30d)
3

Description

**Describe the bug**

`NSItemProvider.loadUIImage()` in `Sources/GravatarUI/SwiftUI/AvatarPicker/SystemImagePicker/PhotosImagePicker.swift` leaks its checked continuation whenever the underlying image load fails, silently dropping the selected photo and leaving the avatar picker in a degraded state with no error surfaced.

```swift
extension NSItemProvider {
fileprivate func loadUIImage() async -> UIImage {
await withCheckedContinuation { continuation in
loadObject(ofClass: UIImage.self) { itemReading, _ in // error discarded
guard let image = itemReading as? UIImage else {
return // ← returns WITHOUT resuming the continuation
}
continuation.resume(returning: image)
}
}
}
}
```

`loadObject(ofClass:completionHandler:)`'s completion is `(NSItemProviderReading?, Error?)`, and `itemReading` is `nil` on failure. The earlier `canLoadObject(ofClass: UIImage.self)` guard (line 43) only advertises type *capability* — it does **not** guarantee the runtime load succeeds. So on a real load failure the `guard`'s `else` returns without calling `continuation.resume(...)`, leaking the continuation.

Because `loadUIImage()` returns a non-optional `UIImage` via `withCheckedContinuation` (non-throwing), the awaiting `Task` in `Coordinator.picker(_:didFinishPicking:)` (lines 49–53) then suspends forever:

- `pickedImage` is never set, so `onImageSelected` never fires; `onCancel` is wired only to the early-`guard` path, so it never fires either.
- The selected photo silently vanishes with **no error or feedback** (the `Error` is discarded as `_` on line 61).
- The runtime logs `SWIFT TASK CONTINUATION MISUSE: loadUIImage() leaked its continuation!`.
- `sourceType` is never reset, so the `.sheet(item:)` binding (keyed on `sourceType`) can be left stuck — re-opening the **same** source (the most natural retry) may fail to re-present.

This is not a main-thread freeze (the `Task` is detached and the app stays responsive), but the pick is lost silently and the picker can be left in a broken state.

**To Reproduce**
Steps to reproduce the behavior:
1. Use a device with iCloud Photos + **Optimize iPhone Storage** enabled (so some originals are not stored locally).
2. Go offline (Airplane Mode), or otherwise prevent the original from downloading.
3. Open the Gravatar avatar picker → **Photo Library**, and select a photo whose original is not cached locally.
4. `loadObject` fails (e.g. `NSItemProviderErrorDomain -1200 "Could not coerce an item to class UIImage"`, or `NSCocoaErrorDomain 260` for a missing file); the continuation leaks and the photo is silently dropped with no feedback.

This is also reachable for assets that fail `UIImage` coercion (e.g. certain RAW/HEIC representations) even after `canLoadObject(ofClass: UIImage.self)` returns `true`.

**Expected behavior**
The continuation should resume on **every** path, and the failure should be handled rather than dropped. For example, make `loadUIImage()` return `UIImage?` (or `throws`) and stop discarding the error:

```swift
fileprivate func loadUIImage() async -> UIImage? {
await withCheckedContinuation { continuation in
loadObject(ofClass: UIImage.self) { itemReading, _ in
continuation.resume(returning: itemReading as? UIImage)
}
}
}
```

…and have the caller handle `nil` — reset `sourceType` so the picker recovers, and surface an error/toast so the user gets feedback. A throwing variant that propagates the `Error` would additionally let the UI distinguish a load failure from a user cancellation.

**Affected version**
Current `trunk` — `PhotosImagePicker.swift` was introduced in #425 (2024-09-26) and the relevant code is unchanged since. Verified against the file at blob `1da49da`.

Contributor guide

Open the contributing guide

Research direction

Start with Sources/GravatarUI/SwiftUI/AvatarPicker/SystemImagePicker/PhotosImagePicker.swift, reading NSItemProvider.loadUIImage() and Coordinator.picker(_:didFinishPicking:). Ensure the load failure resumes the continuation and that the caller handles failure by resetting sourceType and surfacing feedback, then verify the picker can retry the same source.

Written by the indexing model from the issue text.

Assessment

Tech stack
swift
Domain
mobile
Issue type
Bug
Difficulty
3/5
Estimated time
1-2 days
Activity status
Quiet
Clarity
Mostly clear
Newbie friendliness
68/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.