Skip to content
This repository was archived by the owner on Sep 15, 2025. It is now read-only.

Commit 74d1b3d

Browse files
committed
Fix crash caused by using perform(request:...) on background URLSession
1 parent 40d6a9f commit 74d1b3d

File tree

3 files changed

+132
-6
lines changed

3 files changed

+132
-6
lines changed

WordPressKit/HTTPClient.swift

Lines changed: 120 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,18 @@ extension HTTPAPIResponse where Body == Data {
2222

2323
extension URLSession {
2424

25+
/// Create a background URLSession instance that can be used in the `perform(request:...)` async function.
26+
///
27+
/// The `perform(request:...)` async function can be used in all non-background URLSession instances without any
28+
/// extra work. However, there is a requirement to make the function works with with background URLSession instances.
29+
/// That is the URLSession must have a delegate of `BackgroundURLSessionDelegate` type.
30+
static func backgroundSession(configuration: URLSessionConfiguration) -> URLSession {
31+
assert(configuration.identifier != nil)
32+
// Pass `delegateQueue: nil` to get a serial queue, which is required to ensure thread safe access to
33+
// `WordPressKitSessionDelegate` instances.
34+
return URLSession(configuration: configuration, delegate: BackgroundURLSessionDelegate(), delegateQueue: nil)
35+
}
36+
2537
/// Send a HTTP request and return its response as a `WordPressAPIResult` instance.
2638
///
2739
/// ## Progress Tracking and Cancellation
@@ -49,6 +61,10 @@ extension URLSession {
4961
fulfilling parentProgress: Progress? = nil,
5062
errorType: E.Type = E.self
5163
) async -> WordPressAPIResult<HTTPAPIResponse<Data>, E> {
64+
if configuration.identifier != nil {
65+
assert(delegate is BackgroundURLSessionDelegate, "Unexpected URLSession delegate type. See the `backgroundSession(configuration:)`")
66+
}
67+
5268
if let parentProgress {
5369
assert(parentProgress.completedUnitCount == 0 && parentProgress.totalUnitCount > 0, "Invalid parent progress")
5470
assert(parentProgress.cancellationHandler == nil, "The progress instance's cancellationHandler property must be nil")
@@ -97,23 +113,50 @@ extension URLSession {
97113
) throws -> URLSessionTask {
98114
var request = try builder.build(encodeBody: false)
99115

116+
// This additional `callCompletionFromDelegate` is added so that we can test `BackgroundURLSessionDelegate`
117+
// in unit tests. Background URLSession doesn't work on unit tests, we have to create a non-background URLSession
118+
// which has a `BackgroundURLSessionDelegate` delegate in order to test `BackgroundURLSessionDelegate`.
119+
//
120+
// In reality, `callCompletionFromDelegate` and `isBackgroundSession` have the same value.
121+
let callCompletionFromDelegate = delegate is BackgroundURLSessionDelegate
100122
let isBackgroundSession = configuration.identifier != nil
123+
let task: URLSessionTask
101124
let body = try builder.encodeMultipartForm(request: &request, forceWriteToFile: isBackgroundSession)
102125
?? builder.encodeXMLRPC(request: &request, forceWriteToFile: isBackgroundSession)
103126
if let body {
104127
// Use special `URLSession.uploadTask` API for multipart POST requests.
105-
return body.map(
128+
task = body.map(
106129
left: {
107-
uploadTask(with: request, from: $0, completionHandler: completion)
130+
if callCompletionFromDelegate {
131+
return uploadTask(with: request, from: $0)
132+
} else {
133+
return uploadTask(with: request, from: $0, completionHandler: completion)
134+
}
108135
},
109136
right: {
110-
uploadTask(with: request, fromFile: $0, completionHandler: completion)
137+
if callCompletionFromDelegate {
138+
return uploadTask(with: request, fromFile: $0)
139+
} else {
140+
return uploadTask(with: request, fromFile: $0, completionHandler: completion)
141+
}
111142
}
112143
)
113144
} else {
114145
// Use `URLSession.dataTask` for all other request
115-
return dataTask(with: request, completionHandler: completion)
146+
if callCompletionFromDelegate {
147+
task = dataTask(with: request)
148+
} else {
149+
task = dataTask(with: request, completionHandler: completion)
150+
}
151+
}
152+
153+
if callCompletionFromDelegate {
154+
assert(delegate is BackgroundURLSessionDelegate, "Unexpected URLSession delegate type. See the `backgroundSession(configuration:)`")
155+
156+
set(completion: completion, forTaskWithIdentifier: task.taskIdentifier)
116157
}
158+
159+
return task
117160
}
118161

119162
private static func parseResponse<E: LocalizedError>(
@@ -205,3 +248,76 @@ extension Progress {
205248
}
206249
}
207250
}
251+
252+
// MARK: - Background URL Session Support
253+
254+
private final class SessionTaskData {
255+
var responseBody = Data()
256+
var completion: ((Data?, URLResponse?, Error?) -> Void)?
257+
}
258+
259+
class BackgroundURLSessionDelegate: NSObject, URLSessionDataDelegate {
260+
261+
private var taskData = [Int: SessionTaskData]()
262+
263+
func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) {
264+
session.recieved(data, forTaskWithIdentifier: dataTask.taskIdentifier)
265+
}
266+
267+
func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
268+
session.completed(with: error, response: task.response, forTaskWithIdentifier: task.taskIdentifier)
269+
}
270+
271+
}
272+
273+
private extension URLSession {
274+
275+
static var taskDataKey = 0
276+
277+
// A map from `URLSessionTask` identifier to in-memory data of the given task.
278+
//
279+
// This property is in `URLSession` not `BackgroundURLSessionDelegate` because task id (the key) is unique within
280+
// the context of a `URLSession` instance. And in theory `BackgroundURLSessionDelegate` can be used by multiple
281+
// `URLSession` instances.
282+
var taskData: [Int: SessionTaskData] {
283+
get {
284+
objc_getAssociatedObject(self, &URLSession.taskDataKey) as? [Int: SessionTaskData] ?? [:]
285+
}
286+
set {
287+
objc_setAssociatedObject(self, &URLSession.taskDataKey, newValue, .OBJC_ASSOCIATION_RETAIN)
288+
}
289+
}
290+
291+
func updateData(forTaskWithIdentifier taskID: Int, using closure: (SessionTaskData) -> Void) {
292+
let task = self.taskData[taskID] ?? SessionTaskData()
293+
closure(task)
294+
self.taskData[taskID] = task
295+
}
296+
297+
func set(completion: @escaping (Data?, URLResponse?, Error?) -> Void, forTaskWithIdentifier taskID: Int) {
298+
updateData(forTaskWithIdentifier: taskID) {
299+
$0.completion = completion
300+
}
301+
}
302+
303+
func recieved(_ data: Data, forTaskWithIdentifier taskID: Int) {
304+
updateData(forTaskWithIdentifier: taskID) { task in
305+
task.responseBody.append(data)
306+
}
307+
}
308+
309+
func completed(with error: Error?, response: URLResponse?, forTaskWithIdentifier taskID: Int) {
310+
guard let task = taskData[taskID] else {
311+
return
312+
}
313+
314+
if let error {
315+
task.completion?(nil, response, error)
316+
} else {
317+
task.completion?(task.responseBody, response, nil)
318+
}
319+
320+
self.taskData.removeValue(forKey: taskID)
321+
}
322+
323+
}

WordPressKit/WordPressComRestApi.swift

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -335,7 +335,11 @@ open class WordPressComRestApi: NSObject {
335335
private lazy var uploadURLSession: URLSession = {
336336
let configuration = sessionConfiguration(background: backgroundUploads)
337337
configuration.sharedContainerIdentifier = self.sharedContainerIdentifier
338-
return URLSession(configuration: configuration)
338+
if configuration.identifier != nil {
339+
return URLSession.backgroundSession(configuration: configuration)
340+
} else {
341+
return URLSession(configuration: configuration)
342+
}
339343
}()
340344

341345
private func sessionConfiguration(background: Bool) -> URLSessionConfiguration {

WordPressKit/WordPressOrgXMLRPCApi.swift

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,13 @@ open class WordPressOrgXMLRPCApi: NSObject {
4141
additionalHeaders["User-Agent"] = userAgent as AnyObject?
4242
}
4343
sessionConfiguration.httpAdditionalHeaders = additionalHeaders
44-
return URLSession(configuration: sessionConfiguration, delegate: sessionDelegate, delegateQueue: nil)
44+
// When using a background URLSession, we don't need to apply the authentication challenge related
45+
// implementations in `SessionDelegate`.
46+
if sessionConfiguration.identifier != nil {
47+
return URLSession.backgroundSession(configuration: sessionConfiguration)
48+
} else {
49+
return URLSession(configuration: sessionConfiguration, delegate: sessionDelegate, delegateQueue: nil)
50+
}
4551
}
4652

4753
// swiftlint:disable weak_delegate

0 commit comments

Comments
 (0)