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()
}
}
Related
I have a networking layer that currently uses completion handlers to deliver a result on the operation is complete.
As I support a number of iOS versions, I instead extend the network layer within the app to provide support for Combine. I'd like to extend this to now also a support Async/Await but I am struggling to understand how I can achieve this in a way that allows me to cancel requests.
The basic implementation looks like;
protocol HTTPClientTask {
func cancel()
}
protocol HTTPClient {
typealias Result = Swift.Result<(data: Data, response: HTTPURLResponse), Error>
#discardableResult
func dispatch(_ request: URLRequest, completion: #escaping (Result) -> Void) -> HTTPClientTask
}
final class URLSessionHTTPClient: HTTPClient {
private let session: URLSession
init(session: URLSession) {
self.session = session
}
func dispatch(_ request: URLRequest, completion: #escaping (HTTPClient.Result) -> Void) -> HTTPClientTask {
let task = session.dataTask(with: request) { data, response, error in
completion(Result {
if let error = error {
throw error
} else if let data = data, let response = response as? HTTPURLResponse {
return (data, response)
} else {
throw UnexpectedValuesRepresentation()
}
})
}
task.resume()
return URLSessionTaskWrapper(wrapped: task)
}
}
private extension URLSessionHTTPClient {
struct UnexpectedValuesRepresentation: Error {}
struct URLSessionTaskWrapper: HTTPClientTask {
let wrapped: URLSessionTask
func cancel() {
wrapped.cancel()
}
}
}
It very simply provides an abstraction that allows me to inject a URLSession instance.
By returning HTTPClientTask I can call cancel from a client and end the request.
I extend this in a client app using Combine as follows;
extension HTTPClient {
typealias Publisher = AnyPublisher<(data: Data, response: HTTPURLResponse), Error>
func dispatchPublisher(for request: URLRequest) -> Publisher {
var task: HTTPClientTask?
return Deferred {
Future { completion in
task = self.dispatch(request, completion: completion)
}
}
.handleEvents(receiveCancel: { task?.cancel() })
.eraseToAnyPublisher()
}
}
As you can see I now have an interface that supports canceling tasks.
Using async/await however, I am unsure what this should look like, how I can provide a mechanism for canceling requests.
My current attempt is;
extension HTTPClient {
func dispatch(_ request: URLRequest) async -> HTTPClient.Result {
let task = Task { () -> (data: Data, response: HTTPURLResponse) in
return try await withCheckedThrowingContinuation { continuation in
self.dispatch(request) { result in
switch result {
case let .success(values): continuation.resume(returning: values)
case let .failure(error): continuation.resume(throwing: error)
}
}
}
}
do {
let output = try await task.value
return .success(output)
} catch {
return .failure(error)
}
}
}
However this simply provides the async implementation, I am unable to cancel this.
How should this be handled?
Swift’s new concurrency model handles cancellation perfectly well. While the WWDC 2021 videos focused on the checkCancellation and isCancelled patterns (e.g., the Explore structured concurrency in Swift video), in this case, one would use withTaskCancellationHandler to create a task that cancels the network request when the task, itself, is canceled. (Obviously, this is only a concern in iOS 13/14, as in iOS 15 one would just use the provided async methods, data(for:delegate) or data(from:delegate:), which also handle cancelation well.)
See SE-0300: Continuations for interfacing async tasks with synchronous code: Additional Examples for example. That download example is a bit outdated, so here is an updated rendition:
extension URLSession {
#available(iOS, deprecated: 15, message: "Use `data(from:delegate:)` instead")
#available(macOS, deprecated: 12, message: "Use `data(from:delegate:)` instead")
func data(with url: URL) async throws -> (URL, URLResponse) {
try await download(with: URLRequest(url: url))
}
#available(iOS, deprecated: 15, message: "Use `data(for:delegate:)` instead")
#available(macOS, deprecated: 12, message: "Use `data(for:delegate:)` instead")
func data(with request: URLRequest) async throws -> (Data, URLResponse) {
let sessionTask = SessionTask(session: self)
return try await withTaskCancellationHandler {
try await withCheckedThrowingContinuation { continuation in
Task {
await sessionTask.data(for: request) { data, response, error in
guard let data, let response else {
continuation.resume(throwing: error ?? URLError(.badServerResponse))
return
}
continuation.resume(returning: (data, response))
}
}
}
} onCancel: {
Task { await sessionTask.cancel() }
}
}
}
private extension URLSession {
actor SessionTask {
var state: State = .ready
private let session: URLSession
init(session: URLSession) {
self.session = session
}
func cancel() {
if case .executing(let task) = state {
task.cancel()
}
state = .cancelled
}
}
}
// MARK: Data
extension URLSession.SessionTask {
func data(for request: URLRequest, completionHandler: #Sendable #escaping (Data?, URLResponse?, Error?) -> Void) {
if case .cancelled = state {
completionHandler(nil, nil, CancellationError())
return
}
let task = session.dataTask(with: request, completionHandler: completionHandler)
state = .executing(task)
task.resume()
}
}
extension URLSession.SessionTask {
enum State {
case ready
case executing(URLSessionTask)
case cancelled
}
}
A few minor observations on my code snippet:
I gave these names to avoid collision with the iOS 15 method names, but added deprecated messages to inform the developer to use the iOS 15 renditions once you abandon iOS 13/14 support.
I deviated from SE-0300’s example to follow the pattern of the data(from:delegate:) and data(for:delegate:) methods (returning a tuple with Data and a URLResponse).
The actor, not in the original example, is needed to synchronize the access to the URLSessionTask.
Note that according to SE-0304, that regarding withTaskCancellationHandler:
If the task has already been cancelled at the point withTaskCancellationHandler is called, the cancellation handler is invoked immediately, before the operation block is executed.
Because of this, the actor in the above uses a state variable to determine if the request has already been canceled, and just immediately resumes, throwing a CancellationError if it is already canceled.
But all of that is unrelated to the question at hand. In short, use withTaskCancellationHandler.
E.g. Here are five image requests that I started in a task group, as monitored by Charles:
And here are the same requests, but this time I canceled the whole task group (and the cancelations successfully stopped the associated network requests for me):
(Obviously the x-axis scale is different.)
If you need download renditions (to wrap downloadTask), you could do supplement the above with:
extension URLSession {
#available(iOS, deprecated: 15, message: "Use `download(from:delegate:)` instead")
#available(macOS, deprecated: 12, message: "Use `download(from:delegate:)` instead")
func download(with url: URL) async throws -> (URL, URLResponse) {
try await download(with: URLRequest(url: url))
}
#available(iOS, deprecated: 15, message: "Use `download(for:delegate:)` instead")
#available(macOS, deprecated: 12, message: "Use `download(for:delegate:)` instead")
func download(with request: URLRequest) async throws -> (URL, URLResponse) {
let sessionTask = SessionTask(session: self)
return try await withTaskCancellationHandler {
try await withCheckedThrowingContinuation { continuation in
Task {
await sessionTask.download(for: request) { location, response, error in
guard let location, let response else {
continuation.resume(throwing: error ?? URLError(.badServerResponse))
return
}
// since continuation can happen later, let’s figure out where to store it ...
let tempURL = URL(fileURLWithPath: NSTemporaryDirectory())
.appendingPathComponent(UUID().uuidString)
.appendingPathExtension(request.url!.pathExtension)
// ... and move it to there
do {
try FileManager.default.moveItem(at: location, to: tempURL)
} catch {
continuation.resume(throwing: error)
return
}
continuation.resume(returning: (tempURL, response))
}
}
}
} onCancel: {
Task { await sessionTask.cancel() }
}
}
}
extension URLSession.SessionTask {
func download(for request: URLRequest, completionHandler: #Sendable #escaping (URL?, URLResponse?, Error?) -> Void) {
if case .cancelled = state {
completionHandler(nil, nil, CancellationError())
return
}
let task = session.downloadTask(with: request, completionHandler: completionHandler)
state = .executing(task)
task.resume()
}
}
You can't hybridize Combine with async/await. If you embrace async/await fully and call one of the async download methods...
https://developer.apple.com/documentation/foundation/urlsession/3767353-data
...then the task where you call that method will be cancellable in good order through the standard structured concurrency mechanism.
So if you want to support Swift 5.5 / iOS 15 async and yet support earlier versions too, you will need two completely independent implementations of this functionality.
async/await might not be the proper paradigm if you want cancellation. The reason is that the new structured concurrency support in Swift allows you to write code that looks single-threaded/synchronous, but it fact it's multi-threaded.
Take for example a naive synchronous code:
let data = tryData(contentsOf: fileURL)
If the file is huge, then it might take a lot of time for the operation to finish, and during this time the operation cannot be cancelled, and the caller thread is blocked.
Now, assuming Data exports an async version of the above initializer, you'd write the async version of the code similar to this:
let data = try await Data(contentsOf: fileURL)
For the developer, it's the same coding style, once the operation finishes, they'll either have a data variable to use, or they'll be receiving an error.
In both cases, there's no cancellation built in, as the operation is synchronous from the developer's perspective. The major difference is that the await-ed call doesn't block the caller thread, but on the other hand once the control flow returns it might well be that the code continues executing on a different thread.
Now, if you need support for cancellation, then you'll have to store somewhere some identifiable data that can be used to cancel the operation.
If you'll want to store those identifiers from the caller scope, then you'll need to split your operation in two: initialization, and execution.
Something along the lines of
extension HTTPClient {
// note that this is not async
func task(for request: URLRequest) -> HTTPClientTask {
// ...
}
}
class HTTPClientTask {
func dispatch() async -> HTTPClient.Result {
// ...
}
}
let task = httpClient.task(for: urlRequest)
self.theTask = task
let result = await task.dispatch()
// somewhere outside the await scope
self.theTask.cancel()
How can I mock URLSession.DataTaskPublisher? I have a class Proxy that require to inject a URLSessionProtocol
protocol URLSessionProtocol {
func loadData(from url: URL) -> URLSession.DataTaskPublisher
}
class Proxy {
private let urlSession: URLSessionProtocol
init(urlSession: URLSessionProtocol) {
self.urlSession = urlSession
}
func get(url: URL) -> AnyPublisher<Data, ProxyError> {
// Using urlSession.loadData(from: url)
}
}
This code was originally used with the traditional version of URLSession with the completion handler. It was perfect since I could easily mock URLSession for testing like Sundell's solution here: Mocking in Swift.
Is it possible to do the same with the Combine Framework?
In the same way that you can inject a URLSessionProtocol to mock a concrete session, you can also inject a mocked Publisher. For example:
let mockPublisher = Just(MockData()).eraseToAnyPublisher()
However, depending on what you do with this publisher you might have to address some weirdnesses with Combine async publishers, see this post for additional discussion:
Why does Combine's receive(on:) operator swallow errors?
The best way to test your client is to use URLProtocol.
https://developer.apple.com/documentation/foundation/urlprotocol
You can intercept all your request before she performs the real request on the cloud, and so creates your expectation. Once you have done your expectation, she will be destroyed, so you never make real requests.
Tests more reliable, faster, and you got the control!
You got a little example here: https://www.hackingwithswift.com/articles/153/how-to-test-ios-networking-code-the-easy-way
But it's more powerful than just this, you can do everything you want like: Check your Events/Analytics...
I hope it'll help you!
Since DataTaskPublisher uses the URLSession it is created from, you can just mock that. I ended up creating a URLSession subclass, overriding dataTask(...) to return a URLSessionDataTask subclass, which I fed with the data/response/error I needed...
class URLSessionDataTaskMock: URLSessionDataTask {
private let closure: () -> Void
init(closure: #escaping () -> Void) {
self.closure = closure
}
override func resume() {
closure()
}
}
class URLSessionMock: URLSession {
var data: Data?
var response: URLResponse?
var error: Error?
override func dataTask(with request: URLRequest, completionHandler: #escaping (Data?, URLResponse?, Error?) -> Void) -> URLSessionDataTask {
let data = self.data
let response = self.response
let error = self.error
return URLSessionDataTaskMock {
completionHandler(data, response, error)
}
}
}
Then obviously you just want your networking layer using this URLSession, I went with a factory to do this:
protocol DataTaskPublisherFactory {
func make(for request: URLRequest) -> URLSession.DataTaskPublisher
}
Then in your network layer:
func performRequest<ResponseType>(_ request: URLRequest) -> AnyPublisher<ResponseType, APIError> where ResponseType : Decodable {
Just(request)
.flatMap {
self.dataTaskPublisherFactory.make(for: $0)
.mapError { APIError.urlError($0)} } }
.eraseToAnyPublisher()
}
Now you can just pass a mock factory in the test using the URLSession subclass (this one asserts URLErrors are mapped to a custom error, but you could also assert some other condition given data/response):
func test_performRequest_URLSessionDataTaskThrowsError_throwsAPIError() {
let session = URLSessionMock()
session.error = TestError.test
let dataTaskPublisherFactory = mock(DataTaskPublisherFactory.self)
given(dataTaskPublisherFactory.make(for: any())) ~> {
session.dataTaskPublisher(for: $0)
}
let api = API(dataTaskPublisherFactory: dataTaskPublisherFactory)
let publisher: AnyPublisher<TestCodable, APIError> =
api.performRequest(URLRequest(url: URL(string: "www.someURL.com")!))
let _ = publisher.sink(receiveCompletion: {
switch $0 {
case .failure(let error):
XCTAssertEqual(error, APIError.urlError(URLError(_nsError: NSError(domain: "NSURLErrorDomain", code: -1, userInfo: nil))))
case .finished:
XCTFail()
}
}) { _ in }
}
The one issue with this is that URLSession init() is deprecated from iOS 13, so you have to live with a warning in your test. If anyone can see a way around that I'd greatly appreciate it.
(Note: I'm using Mockingbird for mocks).
I'm trying to implement HTTP Basic Auth with NSURLSession, but I run into several issues. Please read the entire question before responding, I doubt this is a duplicate of an other question.
According to the tests I've run, the behavior of NSURLSession is the following :
The first request is always made without the Authorization header.
If the first request fails with a 401 Unauthorized response and a WWW-Authenticate Basic realm=... header, it is automatically retried.
Before retrying the request, the session will attempt to obtain credentials by looking into the NSURLCredentialStorage of the session configuration or by calling the URLSession:task:didReceiveChallenge:completionHandler: delegate method (or both).
If credentials could be obtained the request is retried with the proper Authorization header. If not it is retried without the header (which is weird because in this case, this is exactly the same request).
If the second request succeeds, the task is transparently reported as successful and you're not even notified that the request was attempted twice. If not, the failure of the second request is reported (but not the first).
The problem I have with this behavior is that I am uploading large files to my server through multipart requests, so when the request is attempted twice, the entire POST body is sent twice which is a terrible overhead.
I have tried to manually add the Authorization header to the httpAdditionalHeaders of the session configuration, but it works only if the property is set before the session is created. Attempting to modify session.configuration.httpAdditionalHeaders afterwards doesn't work. Also the documentation clearly says that the Authorization header should not be set manually.
So my question is: If I need to start the session before I obtain the credentials and If I want to be sure that requests are always made with the proper Authorization header the first time, how do I do ?
Here is a code sample that I've used for my tests. You can reproduce all the behaviors I've described above with it.
Note that in order to be able to see the double requests you wil need to either use your own http server and log the requests or connect through a proxy that logs all requests (I've used Charles Proxy for this)
class URLSessionTest: NSObject, URLSessionDelegate
{
static let shared = URLSessionTest()
func start()
{
let requestURL = URL(string: "https://httpbin.org/basic-auth/username/password")!
let credential = URLCredential(user: "username", password: "password", persistence: .forSession)
let protectionSpace = URLProtectionSpace(host: "httpbin.org", port: 443, protocol: NSURLProtectionSpaceHTTPS, realm: "Fake Realm", authenticationMethod: NSURLAuthenticationMethodHTTPBasic)
let useHTTPHeader = false
let useCredentials = true
let useCustomCredentialsStorage = false
let useDelegateMethods = true
let sessionConfiguration = URLSessionConfiguration.default
if (useHTTPHeader) {
let authData = "\(credential.user!):\(credential.password!)".data(using: .utf8)!
let authValue = "Basic " + authData.base64EncodedString()
sessionConfiguration.httpAdditionalHeaders = ["Authorization": authValue]
}
if (useCredentials) {
if (useCustomCredentialsStorage) {
let urlCredentialStorage = URLCredentialStorage()
urlCredentialStorage.set(credential, for: protectionSpace)
sessionConfiguration.urlCredentialStorage = urlCredentialStorage
} else {
sessionConfiguration.urlCredentialStorage?.set(credential, for: protectionSpace)
}
}
let delegate = useDelegateMethods ? self : nil
let session = URLSession(configuration: sessionConfiguration, delegate: delegate, delegateQueue: nil)
self.makeBasicAuthTest(url: requestURL, session: session) {
self.makeBasicAuthTest(url: requestURL, session: session) {
DispatchQueue.main.asyncAfter(deadline: .now() + 61.0) {
self.makeBasicAuthTest(url: requestURL, session: session) {}
}
}
}
}
func makeBasicAuthTest(url: URL, session: URLSession, completion: #escaping () -> Void)
{
let task = session.dataTask(with: url) { (data, response, error) in
if let response = response {
print("response : \(response)")
}
if let data = data {
if let json = try? JSONSerialization.jsonObject(with: data, options: .allowFragments) {
print("json : \(json)")
} else if data.count > 0, let string = String(data: data, encoding: .utf8) {
print("string : \(string)")
} else {
print("data : \(data)")
}
}
if let error = error {
print("error : \(error)")
}
print()
DispatchQueue.main.async(execute: completion)
}
task.resume()
}
#objc(URLSession:didReceiveChallenge:completionHandler:)
func urlSession(_ session: URLSession, didReceive challenge: URLAuthenticationChallenge, completionHandler: #escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Swift.Void)
{
print("Session authenticationMethod: \(challenge.protectionSpace.authenticationMethod)")
if (challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodHTTPBasic) {
let credential = URLCredential(user: "username", password: "password", persistence: .forSession)
completionHandler(.useCredential, credential)
} else {
completionHandler(.performDefaultHandling, nil)
}
}
#objc(URLSession:task:didReceiveChallenge:completionHandler:)
func urlSession(_ session: URLSession, task: URLSessionTask, didReceive challenge: URLAuthenticationChallenge, completionHandler: #escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Swift.Void)
{
print("Task authenticationMethod: \(challenge.protectionSpace.authenticationMethod)")
if (challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodHTTPBasic) {
let credential = URLCredential(user: "username", password: "password", persistence: .forSession)
completionHandler(.useCredential, credential)
} else {
completionHandler(.performDefaultHandling, nil)
}
}
}
Note 1: When making multiple requests in a row to the same endpoint, the behavior I've described above concerns only the first request. Subsequent requests are tried with the proper Authorization header the first time. However, if you wait some time (about 1 minute), the session will return to the default behavior (first request tried twice).
Note 2: This is not directly related, but using a custom NSURLCredentialStorage for the urlCredentialStorage of the session configuration doesn't seem to work. Only using the default value (which is the shared NSURLCredentialStorage according to the documentation) works.
Note 3: I've tried using Alamofire, but since it's based on NSURLSession, it behaves in the exact same way.
If possible, the server should respond with an error long before the client finishes sending the body. However, in many high-level server-side languages, this is difficult, and there's no guarantee that the upload will stop even if you do so.
The real problem is that you're performing a large upload using a single POST request. That make authentication problematic, and also prevents any sort of useful continuation of uploads if the connection drops midway through the upload. Chunking the upload basically solves all of your issues:
For your first request, send only the amount that will fit without adding additional Ethernet packets, i.e. compute your typical header size, mod by 1500 bytes, add a few tens of bytes for good measure, subtract from 1500, and hard-code that size for your first chunk. At most, you've wasted a few packets.
For subsequent chunks, crank the size up.
When a request fails, ask the server how much it got, and retry from where the upload left off.
Issue a request to tell the server when you've finished uploading.
Periodically purge partial uploads on the server side with a cron job or whatever.
That said, if you don't have control over the server side, the usual workaround is to sent an authenticated GET request right before your POST request. This minimizes wasted packets while still mostly working as long as the network is reliable.
I'm trying to set up a timeout for a request method that checks username availability. When the user types in a username and presses a button, the checkUsername method is called. My code is not working because the code inside Timeout(5.0){} is never executed and timeout never gets the value false. I know this is not the best way to do it but I wanted to give it a try and wonder if this can be modified in some way or do I need a different approach?
var timeout: Bool = false
func usernameAvailable(username: String) -> String{
let response: String!
response = Server.checkUsername(username!)
Timeout(5.0){
self.timeout = true
}
while(!timeout){
if(response != nil){
return response
}
}
return "Timeout"
}
The Timeout.swift class looks like this and is working
class Timeout: NSObject{
private var timer: NSTimer?
private var callback: (Void -> Void)?
init(_ delaySeconds: Double, _ callback: Void -> Void){
super.init()
self.callback = callback
self.timer = NSTimer.scheduledTimerWithTimeInterval(NSTimeInterval(delaySeconds),
target: self, selector: "invoke", userInfo: nil, repeats: false)
}
func invoke(){
self.callback?()
// Discard callback and timer.
self.callback = nil
self.timer = nil
}
func cancel(){
self.timer?.invalidate()
self.timer = nil
}
}
I see what you are trying to do and it would make more sense to use an existing framework unless you really need/want to write your own networking code.
I would suggest instead to use the timeoutInterval support in an NSURLRequest along with a completion handler on NSURLSession to achieve the solution that you are seeking.
A timeout of the server response can be handled in the completion handler of something like an NSURLSessionDataTask.
Here is a working example to help get you started that retrieves data from the iTunes Store to illustrate how your timeout could be handled:
let timeout = 5 as NSTimeInterval
let searchTerm = "philip+glass"
let url = NSURL(string: "https://itunes.apple.com/search?term=\(searchTerm)")
let request: NSURLRequest = NSURLRequest(URL: url!,
cachePolicy: NSURLRequestCachePolicy.ReloadIgnoringCacheData,
timeoutInterval: timeout)
let config = NSURLSessionConfiguration.defaultSessionConfiguration()
let session = NSURLSession(configuration: config)
let task: NSURLSessionDataTask = session.dataTaskWithRequest(request, completionHandler: {
(data, response, error) in
if response == nil {
print("Timeout")
} else {
print(String(data: data!, encoding: NSUTF8StringEncoding))
}
}
)
task.resume()
If you reduce the timeout interval to something short, you can force the timeout to happen.
The code in the Timeout block will never run because the timer will fire on the on the main thread, but you're blocking the main thread with your while loop.
You have another issue here, that you're calling Server.checkUsername(username!) and returning that result, which would suggest that this must be a synchronous call (which is not good). So, this is also likely blocking the main thread there. It won't even try to start the Timeout logic until checkUsername returns.
There are kludgy fixes for this, but in my opinion, this begs for a very different pattern. One should never write code that has a spinning while loop that is polling some completion status. It is much better to adopt asynchronous patterns with completionHandler closures. But without more information on what checkUsername is doing, it's hard to get more specific.
But, ideally, if your checkUsername is building a NSMutableURLRequest, just specify timeoutInterval for that and then have the NSURLSessionTask completion block check for NSError with domain of NSURLErrorDomain and a code of NSURLError.TimedOut. You also probably want to cancel the prior request if it's already running.
func startRequestForUsername(username: String, timeout: NSTimeInterval, completionHandler: (Bool?, NSError?) -> ()) -> NSURLSessionTask {
let request = NSMutableURLRequest(URL: ...) // configure your request however appropriate for your web service
request.timeoutInterval = timeout // but make sure to specify timeout
let task = NSURLSession.sharedSession().dataTaskWithRequest(request) { data, response, error in
dispatch_async(dispatch_get_main_queue()) {
guard data != nil && error == nil else {
completionHandler(nil, error)
return
}
let usernameAvailable = ... // parse the boolean success/failure out of the `data` however appropriate
completionHandler(usernameAvailable, nil)
}
}
task.resume()
return task
}
And you can then use it like so:
private weak var previousTask: NSURLSessionTask?
func checkUsername(username: String) {
// update the UI to say that we're checking the availability of the user name here, e.g.
usernameStatus.text = "Checking username availability..."
// now, cancel prior request (if any)
previousTask?.cancel()
// start new request
let task = startRequestForUsername(username, timeout: 5) { usernameAvailable, error in
guard usernameAvailable != nil && error == nil else {
if error?.domain == NSURLErrorDomain && error?.code == NSURLError.TimedOut.rawValue {
// everything is cool, the task just timed out
} else if error?.domain == NSURLErrorDomain && error?.code != NSURLError.Cancelled.rawValue {
// again, everything is cool, the task was cancelled
} else {
// some error other happened, so handle that as you see fit
// but the key issue that if it was `.TimedOut` or `.Cancelled`, then don't do anything
}
return
}
if usernameAvailable! {
// update UI to say that the username is available
self.usernameStatus.text = "Username is available"
} else {
// update UI to say that the username is not available
self.usernameStatus.text = "Username is NOT available"
}
}
// save reference to this task
previousTask = task
}
By the way, if you do this sort of graceful, asynchronous processing of requests, you can also increase the timeout interval (e.g. maybe 10 or 15 seconds). We're not freezing the UI, so we can do whatever we want, and not artificially constrain the time allowed for the request.
Recently I read the source code of Alamofire, and I am really confused about How could Alamofire guarantee the response method would be called in correctly order. I hope someone(maybe matt lol) could help me out.
Example, an easy GET Request like this
Alamofire.request(.GET, "https://api.github.com/users/octocat/received_events")
After I analysed the work flow of it, I posted my understanding
Create the request and underlying NSURLSession.
public func request(method: Method, URLString: URLStringConvertible, parameters: [String: AnyObject]? = nil, encoding: ParameterEncoding = .URL) -> Request {
return Manager.sharedInstance.request(method, URLString, parameters: parameters, encoding: encoding)
}
This method would create a request: Request object, which would contain the underlying NSURLSessionDataTask object. Manager.sharedInstance has already set up a NSURLSession and set itself as that session's delegate. The Manager.sharedInstance object would save a customized object request.delegate in its own property delegate
After those objects were created, Alamofire would send this request immediately.
public func request(URLRequest: URLRequestConvertible) -> Request {
var dataTask: NSURLSessionDataTask?
dispatch_sync(queue) {
dataTask = self.session.dataTaskWithRequest(URLRequest.URLRequest)
}
let request = Request(session: session, task: dataTask!)
delegate[request.delegate.task] = request.delegate
if startRequestsImmediately {
request.resume()
}
return request
}
Since Manager.sharedInstance set itself as the underlying NSURLSession's delegate, when data received, the delegate methods would be called
public func URLSession(session: NSURLSession, dataTask: NSURLSessionDataTask, didReceiveData data: NSData) {
if dataTaskDidReceiveData != nil {
dataTaskDidReceiveData!(session, dataTask, data)
} else if let delegate = self[dataTask] as? Request.DataTaskDelegate {
delegate.URLSession(session, dataTask: dataTask, didReceiveData: data)
}
}
If a user want to get the response and do something related, he would use following public API
// Here the request is a Request object
self.request?.responseString { (request, response, body, error) in
// Something do with the response
}
let's see what the responseString(_: completionHandler:) method does
public func response(queue: dispatch_queue_t? = nil, serializer: Serializer, completionHandler: (NSURLRequest, NSHTTPURLResponse?, AnyObject?, NSError?) -> Void) -> Self {
delegate.queue.addOperationWithBlock {
let (responseObject: AnyObject?, serializationError: NSError?) = serializer(self.request, self.response, self.delegate.data)
dispatch_async(queue ?? dispatch_get_main_queue()) {
completionHandler(self.request, self.response, responseObject, self.delegate.error ?? serializationError)
}
}
return self
}
My question is how 5 could be guaranteed to happen after 3, so the user could get all the response not part of it, because self.response at this time would be fully loaded.
Is it because of NSURLSession's background processing occurs on the same queue -- delegate.queue, which is created like this in Alamofire:
//class Request.TaskDelegate: NSObject, NSURLSessionTaskDelegate
self.queue = {
let operationQueue = NSOperationQueue()
operationQueue.maxConcurrentOperationCount = 1
operationQueue.suspended = true
if operationQueue.respondsToSelector("qualityOfService") {
operationQueue.qualityOfService = NSQualityOfService.Utility
}
return operationQueue
}()
Is my understanding correct, how does that happen? It might be require some understanding about RunLoop and NSURLSession's thread mechanism, if you could point out where I could refer to, thank you as well.