Skip to content

Make URLSession async-compatibility shims cancellation-aware - #136

Merged
alexeichhorn merged 3 commits into
alexeichhorn:mainfrom
Matth-93:fix/urlsession-cancellation
Aug 6, 2026
Merged

alexeichhorn merged 3 commits into
alexeichhorn:mainfrom
Matth-93:fix/urlsession-cancellation

Conversation

@Matth-93

@Matth-93 Matth-93 commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

Problem

The URLSession.data(from:) / data(for:) back-compat shims in AsyncCompatibility.swift wrap dataTask in a plain withCheckedThrowingContinuation with no cancellation handling. When the surrounding Task is cancelled, the underlying URLSessionTask keeps running and downloads to completion — cancellation is silently ignored.

In an app that starts extractions on scroll/focus changes and cancels superseded ones, this leaves "zombie" downloads competing for bandwidth. On a constrained device link they starved foreground work for 20–40s in our testing (an extraction for a title the user had already scrolled past kept running to completion).

Fix

Wrap both shims in withTaskCancellationHandler and cancel the URLSessionTask when the Swift task is cancelled. A small locked box ties the two together and handles the race where cancellation arrives before the dataTask is registered (cancel-on-register). Cancelled requests now fail promptly with URLError(.cancelled).

No public API change; behaviour is identical for non-cancelled calls. The @available bounds already match the shims' existing platform floor (iOS 13 / tvOS 13 / watchOS 6 / macOS 10.15).

Verification

Builds clean across the CI matrix locally, including explicit Swift 6 language mode.


Note

Overview Makes the backward-compatible URLSession async shims cancellation-aware, so cancelling the surrounding Swift task also cancels the underlying network request.

Cancellation handling. Both data(from:) and data(for:) now share performCancellableDataTask, backed by a thread-safe cancellation box that handles cancellation before or after the URLSessionTask is registered. Cancelled requests surface URLError.cancelled instead of continuing to completion.

The data(from:)/data(for:) continuation wrappers ignored Swift task
cancellation entirely — cancelling a Task running an extraction let the
underlying dataTask download to completion anyway. Wrap both in
withTaskCancellationHandler and cancel the URLSessionTask on task
cancellation (guarding the register/cancel race with a locked box), so
cancelled extractions abort their network work immediately with
URLError(.cancelled).
@coderabbitai

coderabbitai Bot commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

Adds a thread-safe cancellation box for in-flight URLSessionTask instances. Updates URLSession.data(from:) and data(for:) to use a shared continuation-based helper. Swift task cancellation now cancels the underlying request. Existing response and error mapping remains unchanged.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title clearly and concisely describes the main change: making URLSession async-compatibility shims cancellation-aware.
Description check ✅ Passed The description directly explains the cancellation problem, the shared helper fix, and the preserved non-cancelled behavior.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (2)
Sources/YouTubeKit/Extensions/AsyncCompatibility.swift (2)

44-83: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Extract shared continuation/cancellation logic to remove duplication.

data(from:) and data(for:) are structurally identical (continuation setup, box registration, resume/cancel handling); only the dataTask(with:...) call differs. Extracting a shared private helper would remove this duplication and keep both call sites in sync going forward.

♻️ Proposed refactor
+    private func performDataTask(
+        _ makeTask: (`@escaping` (Data?, URLResponse?, Error?) -> Void) -> URLSessionDataTask
+    ) async throws -> (Data, URLResponse) {
+        let box = URLSessionTaskCancellationBox()
+        return try await withTaskCancellationHandler {
+            try await withCheckedThrowingContinuation { continuation in
+                let task = makeTask { data, response, error in
+                    guard let data = data, let response = response else {
+                        let error = error ?? URLError(.unknown)
+                        return continuation.resume(throwing: error)
+                    }
+                    continuation.resume(returning: (data, response))
+                }
+                box.register(task)
+                task.resume()
+            }
+        } onCancel: {
+            box.cancel()
+        }
+    }
+
     func data(from url: URL) async throws -> (Data, URLResponse) {
-        let box = URLSessionTaskCancellationBox()
-        return try await withTaskCancellationHandler {
-            try await withCheckedThrowingContinuation { continuation in
-                let task = dataTask(with: url) { data, response, error in
-                    guard let data = data, let response = response else {
-                        let error = error ?? URLError(.unknown)
-                        return continuation.resume(throwing: error)
-                    }
-
-                    continuation.resume(returning: (data, response))
-                }
-                box.register(task)
-                task.resume()
-            }
-        } onCancel: {
-            box.cancel()
-        }
+        try await performDataTask { dataTask(with: url, completionHandler: $0) }
     }

     func data(for request: URLRequest) async throws -> (Data, URLResponse) {
-        let box = URLSessionTaskCancellationBox()
-        return try await withTaskCancellationHandler {
-            try await withCheckedThrowingContinuation { continuation in
-                let task = dataTask(with: request) { data, response, error in
-                    guard let data = data, let response = response else {
-                        let error = error ?? URLError(.unknown)
-                        return continuation.resume(throwing: error)
-                    }
-
-                    continuation.resume(returning: (data, response))
-                }
-                box.register(task)
-                task.resume()
-            }
-        } onCancel: {
-            box.cancel()
-        }
+        try await performDataTask { dataTask(with: request, completionHandler: $0) }
     }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Sources/YouTubeKit/Extensions/AsyncCompatibility.swift` around lines 44 - 83,
Extract the duplicated continuation and cancellation flow from data(from:) and
data(for:) into a shared private helper that accepts the differing dataTask
creation operation, while preserving the existing error handling, task
registration, resume, and cancellation behavior. Update both public overloads to
delegate to this helper.

44-83: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add cancellation-path tests for AsyncCompatibility.swift The current Tests/YouTubeKitTests suite doesn’t cover immediate cancel, mid-flight cancel, or cancel-before-register behavior in URLSessionTaskCancellationBox. A few targeted async tests would protect this race-prone path.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Sources/YouTubeKit/Extensions/AsyncCompatibility.swift` around lines 44 - 83,
Add targeted async tests in the YouTubeKit test suite for
URLSessionTaskCancellationBox covering cancellation before task registration,
immediate cancellation, and cancellation during an in-flight request. Exercise
the AsyncCompatibility data(from:) and data(for:) paths as needed, and assert
requests are cancelled without leaving continuations unresolved.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@Sources/YouTubeKit/Extensions/AsyncCompatibility.swift`:
- Around line 44-83: Extract the duplicated continuation and cancellation flow
from data(from:) and data(for:) into a shared private helper that accepts the
differing dataTask creation operation, while preserving the existing error
handling, task registration, resume, and cancellation behavior. Update both
public overloads to delegate to this helper.
- Around line 44-83: Add targeted async tests in the YouTubeKit test suite for
URLSessionTaskCancellationBox covering cancellation before task registration,
immediate cancellation, and cancellation during an in-flight request. Exercise
the AsyncCompatibility data(from:) and data(for:) paths as needed, and assert
requests are cancelled without leaving continuations unresolved.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 08afae64-7a0a-4cc6-9630-f460168e5585

📥 Commits

Reviewing files that changed from the base of the PR and between 7cc8190 and 0919932.

📒 Files selected for processing (1)
  • Sources/YouTubeKit/Extensions/AsyncCompatibility.swift

@alexeichhorn

Copy link
Copy Markdown
Owner

Nice. Valid problem.

FYI: Will probably in near future anyways get rid of this backport extension as it is only really necessary for a very small subset of setups, if anyone even uses that anymore. But have to check.

But nice fix for the time being. Thanks for your contribution ❤️

@alexeichhorn
alexeichhorn merged commit 46fd1b6 into alexeichhorn:main Aug 6, 2026
20 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants