81

Can I somehow do a synchronous HTTP request via NSURLSession in Swift?

I can do an asynchronous request via the following code:

if let url = NSURL(string: "https://2ch.hk/b/threads.json") {
            let task = NSURLSession.sharedSession().dataTaskWithURL(url) {
                (data, response, error) in

                var jsonError: NSError?
                let jsonDict = NSJSONSerialization.JSONObjectWithData(data, options: nil, error: &jsonError) as [String: AnyObject]
                if jsonError != nil {
                    return
                }

                // ...
            }
            task.resume()
        }

But what about synchronous request?

H. Pauwelyn
  • 13,575
  • 26
  • 81
  • 144
FrozenHeart
  • 19,844
  • 33
  • 126
  • 242
  • Check this out http://stackoverflow.com/questions/21198404/nsurlsession-with-nsblockoperation-and-queues – Ian Nov 06 '14 at 18:11
  • Possible duplicate of [NSURLSession with NSBlockOperation and queues](https://stackoverflow.com/questions/21198404/nsurlsession-with-nsblockoperation-and-queues) – eonil Oct 15 '19 at 10:46

6 Answers6

86

You can use this NSURLSession extension to add a synchronous method:

extension NSURLSession {
    func synchronousDataTaskWithURL(url: NSURL) -> (NSData?, NSURLResponse?, NSError?) {
        var data: NSData?, response: NSURLResponse?, error: NSError?

        let semaphore = dispatch_semaphore_create(0)

        dataTaskWithURL(url) {
            data = $0; response = $1; error = $2
            dispatch_semaphore_signal(semaphore)
        }.resume()

        dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER)

        return (data, response, error)
    }
}

Update for Swift 3:

extension URLSession {
    func synchronousDataTask(with url: URL) -> (Data?, URLResponse?, Error?) {
        var data: Data?
        var response: URLResponse?
        var error: Error?

        let semaphore = DispatchSemaphore(value: 0)

        let dataTask = self.dataTask(with: url) {
            data = $0
            response = $1
            error = $2

            semaphore.signal()
        }
        dataTask.resume()

        _ = semaphore.wait(timeout: .distantFuture)

        return (data, response, error)
    }
}
Nick Keets
  • 3,240
  • 1
  • 14
  • 13
  • 2
    I added this extension to the bottom of my file and replaced `NSURLSession.sharedSession().dataTaskWithURL(url)` with `NSURLSession.sharedSession().synchronousDataTaskWithURL(url)`, but I'm getting an error that there is an extra argument in call. What's the proper way to call this extension function? – tylerSF Jan 30 '16 at 20:14
  • 2
    What you wrote is the proper way to call it, probably the error is elsewhere. Can you provide more context? – Nick Keets Feb 02 '16 at 17:40
  • 2
    The extra argument is the completion handler, which no longer needs to be parameterized in the synchronous call. Calling this method should look as follows (assuming you've declared the required variables): `(data, response, error) = NSURLSession.sharedSession().synchronousDataTaskWithURL(url)` – dave Apr 28 '16 at 23:16
  • Buddy..where does these value for data = $0; response = $1; error = $2 come from. Please explain! – Jayprakash Dubey Jul 14 '16 at 11:26
  • 1
    These are the arguments of the completionHandler callback. See https://developer.apple.com/library/ios/documentation/Foundation/Reference/NSURLSession_class/index.html#//apple_ref/occ/instm/NSURLSession/dataTaskWithURL:completionHandler: – Nick Keets Jul 18 '16 at 07:41
  • If the application went to background and then back to foreground, you make api call this way will cause http load failed error sometimes. If I change method to [NSURLConnection sendSynchronousRequest:XXXX ],the problem will not occur. – Jia Xiao May 11 '18 at 03:55
  • 2
    When I implement this in my own solution my completion handler never gets called. When I turn on CFNetworkingDiagnostics I can see that the URL request successfully completes, but my handler is never executed. Anyone experience this as well or have any guidance towards resolving? Thanks. – JonnyB Jun 12 '18 at 14:11
46

Apple thread discussing the same issue.

+ (NSData *)sendSynchronousRequest:(NSURLRequest *)request  
    returningResponse:(__autoreleasing NSURLResponse **)responsePtr  
    error:(__autoreleasing NSError **)errorPtr {  
    dispatch_semaphore_t    sem;  
    __block NSData *        result;  

    result = nil;  

    sem = dispatch_semaphore_create(0);  

    [[[NSURLSession sharedSession] dataTaskWithRequest:request  
        completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {  
        if (errorPtr != NULL) {  
            *errorPtr = error;  
        }  
        if (responsePtr != NULL) {  
            *responsePtr = response;  
        }  
        if (error == nil) {  
            result = data;  
        }  
        dispatch_semaphore_signal(sem);  
    }] resume];  

    dispatch_semaphore_wait(sem, DISPATCH_TIME_FOREVER);  

   return result;  
}  

Answer by Quinn "The Eskimo!" Apple Developer Relations, Developer Technical Support, Core OS/Hardware

Umar Farooque
  • 2,049
  • 21
  • 32
  • If I need to make to synchronous requests in two asynchronous queues with using of the same instance of the NSURLSession, will the dataTasWithRequest executed asynchronously from inside? – Eugene Biryukov Mar 12 '18 at 14:37
22

Updated one of the answers to use a URLRequest instead, so we can use PUT etc instead.

extension URLSession {
    func synchronousDataTask(urlrequest: URLRequest) -> (data: Data?, response: URLResponse?, error: Error?) {
        var data: Data?
        var response: URLResponse?
        var error: Error?

        let semaphore = DispatchSemaphore(value: 0)

        let dataTask = self.dataTask(with: urlrequest) {
            data = $0
            response = $1
            error = $2

            semaphore.signal()
        }
        dataTask.resume()

        _ = semaphore.wait(timeout: .distantFuture)

        return (data, response, error)
    }
}

I'm calling like this.

var request = URLRequest(url: url1)
request.httpBody = body
request.httpMethod = "PUT"
let (_, _, error) = URLSession.shared.synchronousDataTask(urlrequest: request)
if let error = error {
    print("Synchronous task ended with error: \(error)")
}
else {
    print("Synchronous task ended without errors.")
}
kelin
  • 11,323
  • 6
  • 67
  • 104
Jonny
  • 15,955
  • 18
  • 111
  • 232
  • Is there any particular reason to use `.distantFuture`? Seems to me something a tad larger than the request timeout would make more sense... nobody likes an infinite wait. – Abhi Beckert Mar 03 '20 at 06:46
  • @AbhiBeckert If I'm understanding this correctly —  I think I've got it — the way it works is this: 1. The semaphore is set up with a value of 0. 2. The asynchronous dataTask is set up and called. 3. We reach the semaphore.wait() call. Because 0 more threads are allowed to continue at this point, we wait. 4. At some point in the future, the asynchronous dataTask completes. Inside its completion closure, we call semaphore.signal(), so... 5. Our code from step 3 sees that it is allowed to continue. So the function *as a whole* is synchronous, even if async stuff happens inside. – tuxedobob May 05 '21 at 16:11
  • In other words, the `.distantFuture` is the timeout for the *semaphore*, not the network request. – tuxedobob May 05 '21 at 16:21
9

I want to offer a more modern solution using DispatchGroup.

Usage example 1:

var urlRequest = URLRequest(url: config.pullUpdatesURL)
urlRequest.httpMethod = "GET"
urlRequest.httpBody = requestData
urlRequest.addValue("application/json", forHTTPHeaderField: "Content-Type")
urlRequest.addValue("Bearer \(token)", forHTTPHeaderField: "Authorization")

let (data, response, error) = URLSession.shared.syncRequest(with: urlRequest)

Usage example 2:

let url = URL(string: "https://www.google.com/")
let (data, response, error) = URLSession.shared.syncRequest(with: url)

Extension code:

extension URLSession {
   
   func syncRequest(with url: URL) -> (Data?, URLResponse?, Error?) {
      var data: Data?
      var response: URLResponse?
      var error: Error?
      
      let dispatchGroup = DispatchGroup()
      let task = dataTask(with: url) {
         data = $0
         response = $1
         error = $2
         dispatchGroup.leave()
      }
      dispatchGroup.enter()
      task.resume()
      dispatchGroup.wait()
      
      return (data, response, error)
   }
   
   func syncRequest(with request: URLRequest) -> (Data?, URLResponse?, Error?) {
      var data: Data?
      var response: URLResponse?
      var error: Error?
      
      let dispatchGroup = DispatchGroup()
      let task = dataTask(with: request) {
         data = $0
         response = $1
         error = $2
         dispatchGroup.leave()
      }
      dispatchGroup.enter()
      task.resume()
      dispatchGroup.wait()
      
      return (data, response, error)
   }
   
}

As a bonus, if you need to, you can easily implement a timeout. To do this, you need to use

func wait(timeout: DispatchTime) -> DispatchTimeoutResult instead of func wait()

dronpopdev
  • 797
  • 10
  • 13
-3

Be careful with synchronous requests because it can result in bad user experience but I know sometimes its necessary. For synchronous requests use NSURLConnection:

func synchronousRequest() -> NSDictionary {

        //creating the request
        let url: NSURL! = NSURL(string: "exampledomain/...")
        var request = NSMutableURLRequest(URL: url)
        request.HTTPMethod = "GET"
        request.addValue("application/json", forHTTPHeaderField: "Content-Type")


        var error: NSError?

        var response: NSURLResponse?

        let urlData = NSURLConnection.sendSynchronousRequest(request, returningResponse: &response, error: &error)

        error = nil
        let resultDictionary: NSDictionary = NSJSONSerialization.JSONObjectWithData(urlData!, options: NSJSONReadingOptions.MutableContainers, error: &error) as! NSDictionary

        return resultDictionary
    }
e a
  • 103
  • 9
  • 2
    this is not working on Swift 2 anymore. Apple apparently decided that we don't need synchronous requests. – Duck Jul 22 '15 at 08:54
  • 1
    @SpaceDog thank you for your comment. Could you please explain why Apple does that and tell us the "right way" or provide some helpful tutorial or documentation? Thanks – e a Jul 27 '15 at 14:57
-3

Duplicated answer from https://stackoverflow.com/a/58392835/246776


If semaphore based approach doesn't work for you, try polling based approach.

var reply = Data()
/// We need to make a session object.
/// This is key to make this work. This won't work with shared session.
let conf = URLSessionConfiguration.ephemeral
let sess = URLSession(configuration: conf)
let task = sess.dataTask(with: u) { data, _, _ in
    reply = data ?? Data()
}
task.resume()
while task.state != .completed {
    Thread.sleep(forTimeInterval: 0.1)
}
FileHandle.standardOutput.write(reply)
eonil
  • 83,476
  • 81
  • 317
  • 516