I need to migrate from Alamofire 4 to 5 but I'm missing sessionDidReceiveChallenge callback on the delegate
I used before in version 4 something like this:
let manager = Alamofire.SessionManager(
configuration: URLSessionConfiguration.default
)
manager.delegate.sessionDidReceiveChallenge = { session, challenge in
let method = challenge.protectionSpace.authenticationMethod
if method == NSURLAuthenticationMethodClientCertificate {
return (.useCredential, self.cert.urlCredential())
}
if method == NSURLAuthenticationMethodServerTrust {
let trust = challenge.protectionSpace.serverTrust!
let credential = URLCredential(trust: trust)
return (.useCredential, credential)
}
return (.performDefaultHandling, Optional.none)
}
but now is version 5 the delegate has changed to SessionDelegate class without providing a similar function
I tried to use the delegate from the URLSession like this:
let delegate = SomeSessionDelegate()
let delegateQueue: OperationQueue = .init()
delegateQueue.underlyingQueue = .global(qos: .background)
let session = URLSession(
configuration: URLSessionConfiguration.af.default,
delegate: delegate,
delegateQueue: delegateQueue
)
let manager = Alamofire.Session(
session: session,
delegate: SessionDelegate(),
rootQueue: .global(qos: .background)
)
class SomeSessionDelegate: NSObject, URLSessionDelegate {
let cert = ...
func urlSession(_ session: URLSession, didReceive challenge: URLAuthenticationChallenge, completionHandler: #escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
//same impl as before
}
}
I'm guessing that my implementation in version 5 is wrong because I stopped getting response callback
Please advise on how to manage the request challenge properly in version 5
It isn't necessary to override the SessionDelegate to use client certificates. Alamofire will automatically use an attached URLCredential for client certificate challenges. Just attach the credential to the request:
AF.request(...)
.authenticate(with: clientCertCredential)
.response...
Also, your server trust check will return any trust as valid, which could be a security issue. I'd stop using that code immediately.
For a certificate handling on the session level I used URLProtectionSpace on the URLCredentialStorage shared storage and then set that to Alamofire.Session configuration
here is an example to set it up (port 443 might be enough)
fileprivate func registerURLCredential() {
let storage = URLCredentialStorage.shared
do {
let credential: URLCredential = try loadURLCredential("certificate", password: "blablabla")
let url = URL.API
let host = url.host ?? ""
let ports: [Int] = [80, 443, url.port ?? 0]
for port in ports {
let space = URLProtectionSpace(
host: host,
port: port,
protocol: url.scheme,
realm: nil,
authenticationMethod: NSURLAuthenticationMethodClientCertificate
)
storage.set(credential, for: space)
}
} catch {
print(error)
}
}
fileprivate func createSession(_ configurationHandler: ((_ configuration: URLSessionConfiguration) -> Void)? = nil) -> Alamofire.Session {
let configuration = URLSessionConfiguration.af.default
registerURLCredential()
configuration.urlCredentialStorage = .shared
configurationHandler?(configuration)
let session = Session(
configuration: configuration,
requestQueue: .global(qos: .background),
serializationQueue: .global(qos: .background)
)
return session
}
A simple use for that would like:
let sesstion = createSession({ configuration in
configuration.httpMaximumConnectionsPerHost = 1
})
Related
I append failed requests to queue manager (contains array) in case of no connection
I'm presenting a custom pop-up with a retry button. When the retry button is pressed, I want to retry the requests that cannot be sent in the no connection state. There may be more than one request.
When I try the retryRequest method from Alamofire Session class, the task state of the request remains in the initilazed or finished state, but it must be resumed in order to send the request successfully, how can I solve this situation?
InterceptorInterface.swift
public func didGetNoInternetConnection() {
let viewModel = AppPopupViewModel(title: L10n.AppPopUp.areYouOffline, description: L10n.AppPopUp.checkInternetConnection, image: Images.noInternetConnection.image, firstButtonTitle: L10n.General.tryAgain, secondButtonTitle: nil, firstButtonAction: { [weak self] in
guard let self = self else { return }
DispatchQueue.main.async {
self.retry()
}
}, secondButtonAction: nil, dismissCompletion: nil, dimColor: Colors.appGray.color.withAlphaComponent(0.8), showCloseButton: true, customView: nil)
DispatchQueue.main.async {
AppPopupManager.show(with: viewModel, completion: nil)
}
}
private func retry() {
guard let head = NetworkRequestStorage.shared.head else {
return
}
let request = head.request
let session = head.session
session.retryRequest(request, withDelay: nil)
}
APIInterceptor.swift
final class APIInterceptor: Alamofire.RequestInterceptor {
private let configure: NetworkConfigurable
private var lock = NSLock()
// MARK: - Initilize
internal init(configure: NetworkConfigurable) {
self.configure = configure
}
// MARK: - Request Interceptor Method
internal func adapt(_ urlRequest: URLRequest, for session: Session, completion: #escaping (Result<URLRequest, Error>) -> Void) {
lock.lock()
defer {
lock.unlock()
}
var urlRequest = urlRequest
if let token = self.configure.accessToken {
/// Set the Authorization header value using the access token.
urlRequest.setValue("Bearer " + token, forHTTPHeaderField: "Authorization")
}
// Set Accept-Language header value using language code
urlRequest.setValue(configure.languageCode, forHTTPHeaderField: "Accept-Language")
// Arrange Request logs for develope and staging environment
if let reachability = NetworkReachabilityManager(), !reachability.isReachable {
configure.didGetNoInternetConnection()
completion(.failure(APIClientError.networkError))
}
completion(.success(urlRequest))
}
// MARK: - Error Retry Method
internal func retry(_ request: Request, for session: Session, dueTo error: Error, completion: #escaping (RetryResult) -> Void) {
// Arrange Error logs for develope and staging environment
if let aError = error as? APIClientError, aError.statusCode == 400 { // no connection state
NetworkRequestStorage.shared.enqueue(request: request, session: session)
completion(.doNotRetryWithError(error))
} else {
request.retryCount <= configure.retryCount ? completion(.retry) : completion(.doNotRetryWithError(error))
}
}
}
If the request is successful or there is no connection error, I remove it from the NetworkRequestStoroge class.
I have just shifted to Alamofire 5.
Earlier I used URLSession and Certificate Pinner and to handle auth challenge I used delegate method of URLSessionDelegate with hash values
func urlSession(_ session: URLSession, didReceive challenge: URLAuthenticationChallenge,
completionHandler: #escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
print("being challanged! for \(challenge.protectionSpace.host)")
guard let trust = challenge.protectionSpace.serverTrust else {
print("invalid trust!")
completionHandler(.cancelAuthenticationChallenge, nil)
return
}
let credential = URLCredential(trust: trust)
let pinner = setupCertificatePinner(host: challenge.protectionSpace.host)
if (!pinner.validateCertificateTrustChain(trust)) {
print("failed: invalid certificate chain!")
challenge.sender?.cancel(challenge)
}
if (pinner.validateTrustPublicKeys(trust)) {
completionHandler(.useCredential, credential)
} else {
didPinningFailed = true
print("couldn't validate trust for \(challenge.protectionSpace.host)")
completionHandler(.cancelAuthenticationChallenge, nil)
}
}
Having moved to Alamofire 5, there is no method sessionDidReceiveChallenge which was available in earlier version.
I tried:
private let session: Session = {
let manager = ServerTrustManager(allHostsMustBeEvaluated: true, evaluators:
["devDomain.com": DisabledTrustEvaluator(),
"prodDomain.com": PublicKeysTrustEvaluator()])
let configuration = URLSessionConfiguration.af.default
return Session(configuration: configuration, serverTrustManager: manager)
}()
But I get error:
Error Domain=Alamofire.AFError Code=11 "Server trust evaluation failed due to reason: No public keys were found or provided for evaluation."
Update:
I'd still prefer a way to parse it using 256 fingerprint only, as we get domains and its hashes in first api call.
First you need a ServerTrustEvaluating that handle the certificate pinning a simple implement would be something similar to
public final class CertificatePinnerTrustEvaluator: ServerTrustEvaluating {
public init() {}
func setupCertificatePinner(host: String) -> CertificatePinner {
//get the CertificatePinner
}
public func evaluate(_ trust: SecTrust, forHost host: String) throws {
let pinner = setupCertificatePinner(host: host)
if (!pinner.validateCertificateTrustChain(trust)) {
print("failed: invalid certificate chain!")
throw AFError.serverTrustEvaluationFailed(reason: .noCertificatesFound)
}
if (!pinner.validateTrustPublicKeys(trust)) {
print ("couldn't validate trust for \(host)")
throw AFError.serverTrustEvaluationFailed(reason: .noCertificatesFound)
}
}
}
To be able to use the same evaluator I would suggest to subclass ServerTrustManager to return the same evaluator I did it like this:
class CertificatePinnerServerTrustManager: ServerTrustManager {
let evaluator = CertificatePinnerTrustEvaluator()
init() {
super.init(allHostsMustBeEvaluated: true, evaluators: [:])
}
open override func serverTrustEvaluator(forHost host: String) throws -> ServerTrustEvaluating? {
return evaluator
}
}
after that you should be ready to go by creating the session and passing the manager to it
private let session: Session = {
let trustManager = CertificatePinnerServerTrustManager()
return Session(serverTrustManager: trustManager)
}()
My reference was the method urlSession(_:task:didReceive:completionHandler:) in Alamofire source in SessionDelegate.swift at line 86 (Alamofire V5.2.1)
If you want to pin with public keys you need to provide the certificates from which to parse those public keys in the bundle of your app, or otherwise provide them to PublicKeysTrustEvaluator.
From my Watch, I send commands to my iOS app. It's not clear why but if the app is in the background I can see some errors:
Error Domain=NSURLErrorDomain Code=-997 "Lost connection to background transfer service"
Can't end BackgroundTask: no background task exists with identifier 383 (0x17f), or it may have already been ended. Break in UIApplicationEndBackgroundTaskError() to debug.
I've already tried to change my configuration to background, have a correct identifier for my config.
Static or Lazy implementation of my SessionManager.
Count for deinit on the process.
Network Session manager
static var sessionManager: SessionManager = {
let configuration = URLSessionConfiguration.background(withIdentifier: UUID().uuidString + ".WatchOS_Background")
configuration.httpShouldSetCookies = false
configuration.httpMaximumConnectionsPerHost = 4
configuration.timeoutIntervalForRequest = 50
configuration.networkServiceType = .background
configuration.isDiscretionary = false
configuration.shouldUseExtendedBackgroundIdleMode = true
if #available(iOS 13.0, *) {
configuration.allowsExpensiveNetworkAccess = true
configuration.allowsConstrainedNetworkAccess = true
}
let sessionManager = Alamofire.SessionManager(configuration: configuration)
sessionManager.delegate.sessionDidBecomeInvalidWithError = { _, error in
if let error = error {
print(error)
}
}
sessionManager.delegate.taskDidComplete = { _, task, error in
if let error = error {
print(error)
}
}
return sessionManager
}()
Request example
func getListFromServer(completion: #escaping (ServiceResponse<[Model1]>) -> Void) {
let header: HTTPHeaders = ["User-Agent": UserAgentHelper.fullUserAgentString]
request("/api/1/XXXX", method: .get, parameters: nil, encoding: nil, headers: header).responseData { [weak self] response in
guard let strongSelf = self else { return }
completion(strongSelf.completionResponse(response))
}
}
Request method
#discardableResult private func request(
_ path: String,
method: HTTPMethod,
parameters: Parameters? = nil,
encoding: ParameterEncoding? = nil,
headers: HTTPHeaders? = nil)
-> DataRequest {
let userEncoding = encoding ?? self.defaultEncoding
let task = beginBackgroundTask()
let dataRequest = NetworkService.sessionManager.request("\(API)\(path)",
method: method,
parameters: parameters,
encoding: userEncoding,
headers: headers)
dataRequest.validate()
self.endBackgroundTask(taskID: task)
return dataRequest
}
Begin and end background task
func beginBackgroundTask() -> UIBackgroundTaskIdentifier {
return UIApplication.shared.beginBackgroundTask(withName: "Background_API", expirationHandler: {})
}
func endBackgroundTask(taskID: UIBackgroundTaskIdentifier) {
UIApplication.shared.endBackgroundTask(taskID)
}
I hope to have a proper implementation from your and a stable request life cycle.
Many thanks for your help and sorry in advance for the lack of technical terms.
Your core problem is that you're not properly handling the expiration of your background tasks. You must end the tasks in their expiration handler explicitly:
let task = UIApplication.shared.beginBackgroundTask(withName: "Background_API") {
UIApplication.shared.endBackgroundTask(task)
}
I suggest you read more here, where an Apple DTS engineer has extensively outlined the requirements and edge cases of the background task handling.
Additionally, Alamofire doesn't really support background sessions. Using a foreground session with background task handling is probably your best bet. Once the Alamofire SessionManager is deinitialized, any requests it has started will be cancelled, even for background sessions.
Finally, calling validate() within an Alamofire response handler is invalid. You should be calling it on the request before the response handler is added, as it's validates the response before handlers are called. If you're calling it afterward it won't be able to pass the error it produces to your response handler.
My iOS app uses AVPlayer to play streaming audio from my server and storing it on a device.
I implemented AVAssetResourceLoaderDelegate, so I could intercept the stream. I change my scheme (from http to a fake scheme, so that AVAssetResourceLoaderDelegate method gets called:
func resourceLoader(_ resourceLoader: AVAssetResourceLoader, shouldWaitForLoadingOfRequestedResource loadingRequest: AVAssetResourceLoadingRequest) -> Bool
I followed this tutorial:
http://blog.jaredsinclair.com/post/149892449150/implementing-avassetresourceloaderdelegate-a
Over there, I put the original scheme back, and create a session for pulling audio from the server. Everything works perfectly when my server provides the Content-Length (size of the audio file in bytes) header for the streamed audio file.
But sometimes I stream audio files where I cannot provide their length ahead of time (let's say a live podcast stream). In this case, AVURLAsset sets length to -1 and fails with:
"Error Domain=AVFoundationErrorDomain Code=-11849 \"Operation Stopped\" UserInfo={NSUnderlyingError=0x61800004abc0 {Error Domain=NSOSStatusErrorDomain Code=-12873 \"(null)\"}, NSLocalizedFailureReason=This media may be damaged., NSLocalizedDescription=Operation Stopped}"
And I cannot bypass this error. I tried to go a hacky way, provide fake
Content-Length: 999999999, but in this case, once the entire audio stream is downloaded, my session fails with:
Loaded so far: 10349852 out of 99999999
The request timed out.
//Audio file got downloaded, its size is 10349852
//AVPlayer tries to get the next chunk and then fails with request times out
Have anyone ever faced this problem before?
P.S. If I keep original http scheme in AVURLAsset, AVPlayer knows how to handle this scheme, so it plays audio file just fine (even w/o Content-Length), I do not know how it does that w/o failing. Also, in this case, my AVAssetResourceLoaderDelegate is never used, so I cannot intercept and copy the content of the audio file to a local storage.
Here is the implementation:
import AVFoundation
#objc protocol CachingPlayerItemDelegate {
// called when file is fully downloaded
#objc optional func playerItem(playerItem: CachingPlayerItem, didFinishDownloadingData data: NSData)
// called every time new portion of data is received
#objc optional func playerItemDownloaded(playerItem: CachingPlayerItem, didDownloadBytesSoFar bytesDownloaded: Int, outOf bytesExpected: Int)
// called after prebuffering is finished, so the player item is ready to play. Called only once, after initial pre-buffering
#objc optional func playerItemReadyToPlay(playerItem: CachingPlayerItem)
// called when some media did not arrive in time to continue playback
#objc optional func playerItemDidStopPlayback(playerItem: CachingPlayerItem)
// called when deinit
#objc optional func playerItemWillDeinit(playerItem: CachingPlayerItem)
}
extension URL {
func urlWithCustomScheme(scheme: String) -> URL {
var components = URLComponents(url: self, resolvingAgainstBaseURL: false)
components?.scheme = scheme
return components!.url!
}
}
class CachingPlayerItem: AVPlayerItem {
class ResourceLoaderDelegate: NSObject, AVAssetResourceLoaderDelegate, URLSessionDelegate, URLSessionDataDelegate, URLSessionTaskDelegate {
var playingFromCache = false
var mimeType: String? // is used if we play from cache (with NSData)
var session: URLSession?
var songData: NSData?
var response: URLResponse?
var pendingRequests = Set<AVAssetResourceLoadingRequest>()
weak var owner: CachingPlayerItem?
//MARK: AVAssetResourceLoader delegate
func resourceLoader(_ resourceLoader: AVAssetResourceLoader, shouldWaitForLoadingOfRequestedResource loadingRequest: AVAssetResourceLoadingRequest) -> Bool {
if playingFromCache { // if we're playing from cache
// nothing to do here
} else if session == nil { // if we're playing from url, we need to download the file
let interceptedURL = loadingRequest.request.url!.urlWithCustomScheme(scheme: owner!.scheme!).deletingLastPathComponent()
startDataRequest(withURL: interceptedURL)
}
pendingRequests.insert(loadingRequest)
processPendingRequests()
return true
}
func startDataRequest(withURL url: URL) {
let request = URLRequest(url: url)
let configuration = URLSessionConfiguration.default
configuration.requestCachePolicy = .reloadIgnoringLocalAndRemoteCacheData
configuration.timeoutIntervalForRequest = 60.0
configuration.timeoutIntervalForResource = 120.0
session = URLSession(configuration: configuration, delegate: self, delegateQueue: nil)
let task = session?.dataTask(with: request)
task?.resume()
}
func resourceLoader(_ resourceLoader: AVAssetResourceLoader, didCancel loadingRequest: AVAssetResourceLoadingRequest) {
pendingRequests.remove(loadingRequest)
}
//MARK: URLSession delegate
func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) {
(songData as! NSMutableData).append(data)
processPendingRequests()
owner?.delegate?.playerItemDownloaded?(playerItem: owner!, didDownloadBytesSoFar: songData!.length, outOf: Int(dataTask.countOfBytesExpectedToReceive))
}
func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive response: URLResponse, completionHandler: #escaping (URLSession.ResponseDisposition) -> Void) {
completionHandler(URLSession.ResponseDisposition.allow)
songData = NSMutableData()
self.response = response
processPendingRequests()
}
func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError err: Error?) {
if let error = err {
print(error.localizedDescription)
return
}
processPendingRequests()
owner?.delegate?.playerItem?(playerItem: owner!, didFinishDownloadingData: songData!)
}
//MARK:
func processPendingRequests() {
var requestsCompleted = Set<AVAssetResourceLoadingRequest>()
for loadingRequest in pendingRequests {
fillInContentInforation(contentInformationRequest: loadingRequest.contentInformationRequest)
let didRespondCompletely = respondWithDataForRequest(dataRequest: loadingRequest.dataRequest!)
if didRespondCompletely {
requestsCompleted.insert(loadingRequest)
loadingRequest.finishLoading()
}
}
for i in requestsCompleted {
pendingRequests.remove(i)
}
}
func fillInContentInforation(contentInformationRequest: AVAssetResourceLoadingContentInformationRequest?) {
// if we play from cache we make no URL requests, therefore we have no responses, so we need to fill in contentInformationRequest manually
if playingFromCache {
contentInformationRequest?.contentType = self.mimeType
contentInformationRequest?.contentLength = Int64(songData!.length)
contentInformationRequest?.isByteRangeAccessSupported = true
return
}
// have no response from the server yet
if response == nil {
return
}
let mimeType = response?.mimeType
contentInformationRequest?.contentType = mimeType
if response?.expectedContentLength != -1 {
contentInformationRequest?.contentLength = response!.expectedContentLength
contentInformationRequest?.isByteRangeAccessSupported = true
} else {
contentInformationRequest?.isByteRangeAccessSupported = false
}
}
func respondWithDataForRequest(dataRequest: AVAssetResourceLoadingDataRequest) -> Bool {
let requestedOffset = Int(dataRequest.requestedOffset)
let requestedLength = dataRequest.requestedLength
let startOffset = Int(dataRequest.currentOffset)
// Don't have any data at all for this request
if songData == nil || songData!.length < startOffset {
return false
}
// This is the total data we have from startOffset to whatever has been downloaded so far
let bytesUnread = songData!.length - Int(startOffset)
// Respond fully or whaterver is available if we can't satisfy the request fully yet
let bytesToRespond = min(bytesUnread, requestedLength + Int(requestedOffset))
dataRequest.respond(with: songData!.subdata(with: NSMakeRange(startOffset, bytesToRespond)))
let didRespondFully = songData!.length >= requestedLength + Int(requestedOffset)
return didRespondFully
}
deinit {
session?.invalidateAndCancel()
}
}
private var resourceLoaderDelegate = ResourceLoaderDelegate()
private var scheme: String?
private var url: URL!
weak var delegate: CachingPlayerItemDelegate?
// use this initializer to play remote files
init(url: URL) {
self.url = url
let components = URLComponents(url: url, resolvingAgainstBaseURL: false)!
scheme = components.scheme
let asset = AVURLAsset(url: url.urlWithCustomScheme(scheme: "fakeScheme").appendingPathComponent("/test.mp3"))
asset.resourceLoader.setDelegate(resourceLoaderDelegate, queue: DispatchQueue.main)
super.init(asset: asset, automaticallyLoadedAssetKeys: nil)
resourceLoaderDelegate.owner = self
self.addObserver(self, forKeyPath: "status", options: NSKeyValueObservingOptions.new, context: nil)
NotificationCenter.default.addObserver(self, selector: #selector(didStopHandler), name:NSNotification.Name.AVPlayerItemPlaybackStalled, object: self)
}
// use this initializer to play local files
init(data: NSData, mimeType: String, fileExtension: String) {
self.url = URL(string: "whatever://whatever/file.\(fileExtension)")
resourceLoaderDelegate.songData = data
resourceLoaderDelegate.playingFromCache = true
resourceLoaderDelegate.mimeType = mimeType
let asset = AVURLAsset(url: url)
asset.resourceLoader.setDelegate(resourceLoaderDelegate, queue: DispatchQueue.main)
super.init(asset: asset, automaticallyLoadedAssetKeys: nil)
resourceLoaderDelegate.owner = self
self.addObserver(self, forKeyPath: "status", options: NSKeyValueObservingOptions.new, context: nil)
NotificationCenter.default.addObserver(self, selector: #selector(didStopHandler), name:NSNotification.Name.AVPlayerItemPlaybackStalled, object: self)
}
func download() {
if resourceLoaderDelegate.session == nil {
resourceLoaderDelegate.startDataRequest(withURL: url)
}
}
override init(asset: AVAsset, automaticallyLoadedAssetKeys: [String]?) {
fatalError("not implemented")
}
// MARK: KVO
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
delegate?.playerItemReadyToPlay?(playerItem: self)
}
// MARK: Notification handlers
func didStopHandler() {
delegate?.playerItemDidStopPlayback?(playerItem: self)
}
// MARK:
deinit {
NotificationCenter.default.removeObserver(self)
removeObserver(self, forKeyPath: "status")
resourceLoaderDelegate.session?.invalidateAndCancel()
delegate?.playerItemWillDeinit?(playerItem: self)
}
}
You can not handle this situation as for iOS this file is damaged because header is incorrect. System think that you are going to play regular audio file but it doesn't have all info about it. You don't know what audio duration will be, only if you have a live streaming. Live streaming on iOS is done using HTTP live streaming protocol.
Your iOS code is correct. You have to modify your backend and provide m3u8 playlist for live streaming audios, then iOS will accept it as a live stream and audio player will start tracks.
Some related info can be found here. As an iOS developer with good experience in streaming audio / video I can tell you that code to play live / VOD is the same.
But sometimes I stream audio files where I cannot provide their length ahead of time (let's say a live podcast stream). In this case, AVURLAsset sets length to -1 and fails with
In this scenario you should let the player re-request this data later on and set renewalDate property of contentInformationRequest for the given part to some point in future when this data will be available.
If it's just an inifinte live stream, you always provide the length of aquired portion, and set new renewDate for the next renewal cycle (according to my observation natively AVPlayer just updates this data with fixed period of time, say, every 4-6 seconds). The server usually provides such information with "Expires" http header. You can rely on this information yourself and implement something like this (borrowed from my own question on apple developers forum):
if let httpResonse = response as? HTTPURLResponse, let expirationValue = httpResonse.value(forHTTPHeaderField: "Expires") {
let dateFormatter = DateFormatter()
dateFormatter.locale = Locale(identifier: "en_US_POSIX")
dateFormatter.dateFormat = "EEE, dd MMM yyyy HH:mm:ss zzz"
if let expirationDate = dateFormatter.date(from: expirationValue) {
let renewDate = max(expirationDate, Date(timeIntervalSinceNow: 8))
contentInformationRequest.renewalDate = renewDate
}
}
This line let renewDate = max(expirationDate, Date(timeIntervalSinceNow: 8)) adds 8 seconds grace period for the
player to load videos. Otherwise it does not keep up with the pace of
renewals, and video loads in poor quality.
Or just update it periodically if you know in advance it's a live asset without fixed length and your server doesn't privde the required information:
contentInformationRequest.renewalDate = Date(timeIntervalSinceNow: 8)
I am trying to connect to the Twitter streaming API endpoint. It looks like URLSession supports streaming via URLSessionStreamTask, however I can't figure out how to use the API. I have not been able to find any sample code either.
I tried testing the following, but there is no network traffic recorded:
let session = URLSession(configuration: .default, delegate: self, delegateQueue: nil)
let stream = session.streamTask(withHostName: "https://stream.twitter.com/1.1/statuses/sample.json", port: 22)
stream.startSecureConnection()
stream.readData(ofMinLength: 0, maxLength: 100000, timeout: 60, completionHandler: { (data, bool, error) in
print("bool = \(bool)")
print("error = \(String(describing: error))")
})
stream.resume()
I've also implemented the delegate methods (including URLSessionStreamDelegate), but they do not get called.
It would be really helpful if someone code post a sample of how to open a persistent connection for chunked responses from a streaming endpoint. Also, I am seeking solutions which don't involve third party libraries. A response similar to https://stackoverflow.com/a/9473787/5897233 but updated with the URLSession equivalent would be ideal.
Note: Authorization info was omitted from the sample code above.
Received lots of info courtesy of Quinn "The Eskimo" at Apple.
Alas, you have the wrong end of the stick here. URLSessionStreamTask is for wrangling a naked TCP (or TLS over TCP) connection, without the HTTP framing on top. You can think of it as a high-level equivalent to the BSD Sockets API.
The chunked transfer encoding is part of HTTP, and is thus supported by all of the other URLSession task types (data task, upload task, download task). You don’t need to do anything special to enable this. The chunked transfer encoding is a mandatory part of the HTTP 1.1 standard, and is thus is always enabled.
You do, however, have an option as to how you receive the returned data. If you use the URLSession convenience APIs (dataTask(with:completionHandler:) and so on), URLSession will buffer all the incoming data and then pass it to your completion handler in one large Data value. That’s convenient in many situations but it doesn’t work well with a streamed resource. In that case you need to use the URLSession delegate-based APIs (dataTask(with:) and so on), which will call the urlSession(_:dataTask:didReceive:) session delegate method with chunks of data as they arrive.
As for the specific endpoint I was testing, the following was uncovered:
It seems that the server only enables its streaming response (the chunked transfer encoding) if the client sends it a streaming request. That’s kinda weird, and definitely not required by the HTTP spec.
Fortunately, it is possible to force URLSession to send a streaming request:
Create your task with uploadTask(withStreamedRequest:)
Implement the urlSession(_:task:needNewBodyStream:) delegate method to return an input stream that, when read, returns the request body
Profit!
I’ve attached some test code that shows this in action. In this case it uses a bound pair of streams, passing the input stream to the request (per step 2 above) and holding on to the output stream.
If you want to actually send data as part of the request body you can do so by writing to the output stream.
class NetworkManager : NSObject, URLSessionDataDelegate {
static var shared = NetworkManager()
private var session: URLSession! = nil
override init() {
super.init()
let config = URLSessionConfiguration.default
config.requestCachePolicy = .reloadIgnoringLocalCacheData
self.session = URLSession(configuration: config, delegate: self, delegateQueue: .main)
}
private var streamingTask: URLSessionDataTask? = nil
var isStreaming: Bool { return self.streamingTask != nil }
func startStreaming() {
precondition( !self.isStreaming )
let url = URL(string: "ENTER STREAMING URL HERE")!
let request = URLRequest(url: url)
let task = self.session.uploadTask(withStreamedRequest: request)
self.streamingTask = task
task.resume()
}
func stopStreaming() {
guard let task = self.streamingTask else {
return
}
self.streamingTask = nil
task.cancel()
self.closeStream()
}
var outputStream: OutputStream? = nil
private func closeStream() {
if let stream = self.outputStream {
stream.close()
self.outputStream = nil
}
}
func urlSession(_ session: URLSession, task: URLSessionTask, needNewBodyStream completionHandler: #escaping (InputStream?) -> Void) {
self.closeStream()
var inStream: InputStream? = nil
var outStream: OutputStream? = nil
Stream.getBoundStreams(withBufferSize: 4096, inputStream: &inStream, outputStream: &outStream)
self.outputStream = outStream
completionHandler(inStream)
}
func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) {
NSLog("task data: %#", data as NSData)
}
func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
if let error = error as NSError? {
NSLog("task error: %# / %d", error.domain, error.code)
} else {
NSLog("task complete")
}
}
}
And you can call the networking code from anywhere such as:
class MainViewController : UITableViewController {
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
if NetworkManager.shared.isStreaming {
NetworkManager.shared.stopStreaming()
} else {
NetworkManager.shared.startStreaming()
}
self.tableView.deselectRow(at: indexPath, animated: true)
}
}
Hope this helps.
So, this is a lot less robust than the example with no explicit task canceling or writing to stream but if you're just YOLO listening to a Server Sent Event stream, this works as of Feb of 2023. It's based on "Use async/await with URLSession" WWDC21 session. That session also has an example for using a custom delegate.
https://developer.apple.com/videos/play/wwdc2021/10095/
func streamReceiverTest(streamURL:URL, session:URLSession) async throws {
let (bytes, response) = try await session.bytes(from:streamURL)
guard let httpResponse = response as? HTTPURLResponse else {
throw APIngError("Not an HTTPResponse")
}
guard httpResponse.statusCode == 200 else {
throw APIngError("Not a success: \(httpResponse.statusCode)")
}
for try await line in bytes.lines {
print(line)
print()
}
}
Inspecting the request with try await streamReceiverTest(streamURL:URL(string:"https://httpbin.org/get")!, session:URLSession.shared) doesn't show that the Accepts header is set, but it seems the API I'm using does need that to offer the stream. Some servers might(?) so I'll include that version as well.
func streamReceiverTestWithManualHeader(streamURL:URL, session:URLSession) async throws {
var request = URLRequest(url:streamURL)
request.setValue("text/event-stream", forHTTPHeaderField:"Accept")
let (bytes, response) = try await session.bytes(for:request)
guard let httpResponse = response as? HTTPURLResponse else {
throw APIngError("Not an HTTPResponse")
}
guard httpResponse.statusCode == 200 else {
throw APIngError("Not a success: \(httpResponse.statusCode)")
}
for try await line in bytes.lines {
print(line)
print()
}
}