URLSessionDownloadDelegate never called

My request call site:

        let headers = coordinator.requestHeaders(path: headersPath)
        var request = URLRequest(url: url, cachePolicy: .useProtocolCachePolicy, timeoutInterval: defaultTimeoutInterval)
        request.httpMethod = HTTPRequestMethod.get.rawValue
        request.allHTTPHeaderFields = headers
        
        let delegate = SessionDelegate(priority: priority, progressHandler: progressHandler)
        let session = URLSession(configuration: configuration, delegate: delegate, delegateQueue: .main)
        let (localUrl, response) = try await session.download(for: request)
        let data = try Data(contentsOf: localUrl)

SessionDelegate:

public class SessionDelegate: NSObject, URLSessionTaskDelegate, URLSessionDownloadDelegate {
    public typealias ProgressHandler = ((_ progress: Double) -> Void)
    
    public var metrics: URLSessionTaskMetrics?
    
    private let priority: TaskPriority
    private let progressHandler: ProgressHandler?
    
    //MARK: - Lifecycle
    init(priority: TaskPriority, progressHandler: ProgressHandler? = nil) {
        self.priority = priority
        self.progressHandler = progressHandler
    }
    
    //MARK: - URLSessionTaskDelegate
    public func urlSession(_ session: URLSession, didCreateTask task: URLSessionTask) {
        task.priority = priority.rawValue
    }
    
    public func urlSession(_ session: URLSession, task: URLSessionTask, didFinishCollecting metrics: URLSessionTaskMetrics) {
        self.metrics = metrics
    }
    
    public func urlSession(_ session: URLSession, task: URLSessionTask, didSendBodyData bytesSent: Int64, totalBytesSent: Int64, totalBytesExpectedToSend: Int64) {
        if let progressHandler {
            let progress = Progress(totalUnitCount: totalBytesExpectedToSend)
            progress.completedUnitCount = totalBytesSent
            progressHandler(progress.fractionCompleted)
        }
    }
    
    //MARK: - URLSessionDownloadDelegate
    public func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) { }
    
    public func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didWriteData bytesWritten: Int64, totalBytesWritten: Int64, totalBytesExpectedToWrite: Int64) {
        if let progressHandler {
            let progress = Progress(totalUnitCount: totalBytesExpectedToWrite)
            progress.completedUnitCount = totalBytesWritten
            progressHandler(progress.fractionCompleted)
        }
    }
}

For some reason I get delegate callbacks from URLSessionDelegate and URLSessionTaskDelegate but never from URLSessionDownloadDelegate. I've tried assigning the delegate to the task or the session (or both), but behavior remains the same.
Not sure if related but when I try using background session configuration I crash on let (localUrl, response) = try await session.download(for: request) with the error Completion handler blocks are not supported in background sessions. Use a delegate instead. even though I use the async version of the download task, and assigning delegate to the session, it might point to the reason why none of the delegate functions is getting called (maybe?), but I have no clue why Xcode thinks I'm using completion handler.

Answered by DTS Engineer in 766806022

You’re using the Swift concurrency interface to URLSession, which is layered on top of the convenience APIs. Those APIs do not call trigger certain delegate methods. For example, urlSession(_:downloadTask:didFinishDownloadingTo:) is not called because the convenience method passes the URL to the completion handler.

Looking at your code, you immediately read the contents of the download into memory. Is that just to demo this problem? Or is that how your real app works? Because, if so, there’s not much point downloading to a file in the first place.

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

Accepted Answer

You’re using the Swift concurrency interface to URLSession, which is layered on top of the convenience APIs. Those APIs do not call trigger certain delegate methods. For example, urlSession(_:downloadTask:didFinishDownloadingTo:) is not called because the convenience method passes the URL to the completion handler.

Looking at your code, you immediately read the contents of the download into memory. Is that just to demo this problem? Or is that how your real app works? Because, if so, there’s not much point downloading to a file in the first place.

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

Hi @eskimo, thanks for the response.
About the swift concurrency interface, the fact that those APIs do not trigger certain delegate methods is really confusing, it is not documented properly and what makes it even more confusing is that delegate is one of the function parameters.
I would like to calculate download progress, and prefer doing it using delegates instead of KVO. is there an easy way doing this using concurrency convenience functions?.
About the way I structured my download task, it is a work in progress, if you have a good reference for downloading in chunks using concurrency I'll be more than happy to take a look, I guess it involves delegates like didReceive data and saving to FileManager?
Does the fact that I read the contents of the download immediately into memory prevent urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didWriteData bytesWritten: Int64, totalBytesWritten: Int64, totalBytesExpectedToWrite: Int64) from getting called?

Thanks.

what makes it even more confusing

Agreed. I encourage you to file a bug against the documentation about this.

Please post your bug number, just for the record.

is there an easy way doing this using concurrency convenience functions?

Not that I’ve seen.


About the way I structured my download task, it is a work in progress, if you have a good reference for downloading in chunks using concurrency I'll be more than happy to take a look … ?

You can do that using bytes(for:delegate:).

However, my concern wasn’t that you’re downloading to file — that’s perfectly reasonable — but that you then turn around and read the entire file into memory. That’s counter productive. Either the download is expected to be small enough to fit into memory, in which case you should just fetch it to memory, or it’s big enough to warrant downloading to a file, in which case you shouldn’t turn around and load it it all into memory.

Does the fact that I read the contents of the download immediately into memory prevent … didWriteData … from getting called?

No. I’m not 100% sure what’s going on with the delegate method.

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

Iv'e sent a feedback, FB13228209.

but that you then turn around and read the entire file into memory. That’s counter productive

For the time being our average download size is about 4MB, mostly photos and attachments, and we do limit maximum upload size, but I get your point.
I will find a way to avoid loading file data directly providing a local file url instead.
Thanks.

URLSessionDownloadDelegate never called
 
 
Q