Search code examples
swiftnsurlsessionnsjsonserialization

Downloading JSON data fails at NSHTTPURLResponse


I am trying to get a JSON file from a URL and return the contents using Swift. However, the code fails at the line let httpResponse = response as! NSHTTPURLResponse in the following code. I get an exception at this line and Xcode goes into debug mode.

class func downloadJSONFile()->AnyObject
    {
        let requestURL: NSURL = NSURL(string: "http://www.learnswiftonline.com/Samples/subway.json")!
        let urlRequest: NSMutableURLRequest = NSMutableURLRequest(URL: requestURL)
        let session = NSURLSession.sharedSession()
        var json:AnyObject = ""
        let task = session.dataTaskWithRequest(urlRequest) {
            (data, response, error) -> Void in

            let httpResponse = response as! NSHTTPURLResponse
            let statusCode = httpResponse.statusCode

            if (statusCode == 200) {

                do{
                    json = try NSJSONSerialization.JSONObjectWithData(data!, options:.AllowFragments)

                }catch {
                    print("Error with Json: \(error)")

                }

            }

        }

        task.resume()

        return json
    }

How can I fix this?


Solution

  • There are a few issues:

    1. If there is any error in the request, response will be nil, and thus your attempt to force cast it will result in fatal error. Do not use forced unwrapping/casting when dealing with network responses.

    2. There is a deeper problem here that you're trying to return data from a method that runs asynchronously. You should change your method to not return anything, per se, but rather supply a completion handler by which you can asynchronously pass back the relevant data:

       class func downloadJSONFile(completionHandler: @escaping (Any?) -> Void) {
           let requestURL = URL(string: "http://www.learnswiftonline.com/Samples/subway.json")!
           let urlRequest = URLRequest(url: requestURL)
           let session = URLSession.shared
           let task = session.dataTask(with: urlRequest) { data, response, error in
               // check for fundamental networking errors
      
               guard
                   error == nil,
                   let data = data
               else {
                   print(error ?? "Other error")
                   completionHandler(nil)
                   return
               }
      
               guard
                   let httpResponse = response as? HTTPURLResponse,
                   (200 ..< 300) ~= httpResponse.statusCode
               else {
                   print("Invalid status code")
                   completionHandler(nil)
                   return
               }
      
               do {
                   let json = try JSONSerialization.jsonObject(with: data)
                   completionHandler(json)
               } catch let parseError {
                   print("Error parsing: \(parseError)")
                   completionHandler(nil)
               }
           }
      
           task.resume()
       }
      

      and then you call it, using the completion handler (or use trailing closure syntax, like shown below):

       APIClass.downloadJSONFile() { json in
           guard json != nil else {
               print("there was some problem")
               return
           }
      
           // now you can use `json` here
      
           dispatch_async(dispatch_get_main_queue()) {
               // and if you're doing any model or UI updates, dispatch that back to the main queue
           }
       }
      
       // but do not use `json` here, as the above runs asynchronously
      
    3. Note, if you wanted to supply the error information back to the calling routine, you could change it to also return the error information, e.g.:

      enum DownloadError: Error {
          case networkError(Error)
          case notHTTPResponse
          case invalidHTTPResponse(Int)
          case parseError(Error)
      }
      
      class func downloadJSONFile(completionHandler: @escaping (Result<Any, Error>) -> Void) {
          let requestURL = URL(string: "http://www.learnswiftonline.com/Samples/subway.json")!
          let urlRequest = URLRequest(url: requestURL)
          let session = URLSession.shared
          let task = session.dataTask(with: urlRequest) { data, response, error in
              if let error = error {
                  completionHandler(.failure(DownloadError.networkError(error)))
                  return
              }
      
              guard let httpResponse = response as? HTTPURLResponse, let data = data else {
                  completionHandler(.failure(DownloadError.notHTTPResponse))
                  return
              }
      
              guard 200 ..< 300 ~= httpResponse.statusCode else {
                  completionHandler(.failure(DownloadError.invalidHTTPResponse(httpResponse.statusCode)))
                  return
              }
      
              do {
                  let json = try JSONSerialization.jsonObject(with: data)
                  completionHandler(.success(json))
              } catch let parseError {
                  completionHandler(.failure(DownloadError.parseError(parseError)))
              }
          }
      
          task.resume()
      }
      

      And, obviously, the call would change to take both parameters:

      APIClass.downloadJSONFile() { result in
          switch result {
          case .failure(let error):
              print(error)
      
          case .success(let value):
              // and then it would be like before ...
          }
      }
      
    4. When using URLSession in iOS 9 and later, it will not permit cleartext requests (i.e. "http" is not permitted, only "https" is, by default). You can force the app to permit non-https requests by adding the following to your info.plist. See https://stackoverflow.com/a/31254874/1271826 for more information

       <key>NSAppTransportSecurity</key>
       <dict>
           <key>NSExceptionDomains</key>
           <dict>
               <key>learnswiftonline.com</key>
               <dict>
                   <!--Include to allow subdomains-->
                   <key>NSIncludesSubdomains</key>
                   <true/>
                   <!--Include to allow HTTP requests-->
                   <key>NSTemporaryExceptionAllowsInsecureHTTPLoads</key>
                   <true/>
                   <!--Include to specify minimum TLS version-->
                   <key>NSTemporaryExceptionMinimumTLSVersion</key>
                   <string>TLSv1.1</string>
               </dict>
           </dict>
       </dict>