drmohundro / SWXMLHash

Simple XML parsing in Swift
MIT License
1.41k stars 205 forks source link

Async NSXML can’t work in sync mode, please add completion handlers #25

Closed alusev closed 6 years ago

alusev commented 9 years ago

Sometimes “parse” method returns an empty object (every 4-5th execution)

Code: let xml = SWXMLHash.parse(data)

Log: (lldb) po xml Element { Element = 0x00007f93f2e18960 { name = "SWXMLHash_Root_Element" text = nil attributes = {} children = 0 values {} count = 0 index = 0 } }

(lldb) po data <OS_dispatch_data: data[0x7f93f2e29830] = { composite, size = 2031, num_records = 2 record[0] = { from = 0, length = 1297, data_object = 0x7f93f2ccdff0 }, record[1] = { from = 0, length = 734, data_object = 0x7f93f2e157c0 }, }>

drmohundro commented 9 years ago

I'm using NSXMLParser underneath which, if my understanding is correct, is synchronous. There aren't any other uses of async code that I can think of in parse either, so I don't think the error is related to async handling unless you're specifically using it in asynchronous code and seeing the error.

Related to the error you're seeing, you mention that you're calling parse getting blank results back on the 4th or 5th call. Are you passing the same data instance in each time?

(As an aside, if it is the same data being passed in, you should be able to get away with only calling parse once and then saving off the return value.)

alusev commented 9 years ago

You’re right, it is synchronous, then I don’t understand why it may return an empty object. I use the library on iOS for parsing a response from web-service and the response is always relatively small. Each time I do 2 SOAP calls (and then I parse it): 1st time I get a token and the 2nd time I get data that I need. There are 2 different functions within a class. So every time it is a different XMLIndexer object. Sometimes it fails to parse in the first function but it is more common that it fails on the 2nd. Sometimes it fails on the very first call (token + data), and it fails every time until I rerun my app, sometimes it fails only once, another time it fails after 4-5 calls and then it works again.

drmohundro commented 9 years ago

Do you have any code that I could use to help repro this, like just a snippet?

It's possible there is a bug where the library is incorrectly caching something, but nothing comes to mind right away.

alusev commented 9 years ago

Sure. Here is it.

class Fetcher {
    init() {}

    func startBalanceRequest(phoneNumber: String, completionHandler: (phoneNumber: String, data: Dictionary<String, AnyObject>?, errorMessage: String?) -> Void) {
        let today = dateFormatter.stringFromDate(NSDate())
        let envelope = "<soapenv:Envelope xmlns:soapenv='http://schemas.xmlsoap.org/soap/envelope/'> ... </soapenv:Envelope>"

        let request = NSMutableURLRequest(URL: NSURL(string: WebserviceConstants.WebserviceURL)!)
        request.HTTPMethod = "POST"
        request.HTTPBody = envelope.dataUsingEncoding(NSUTF8StringEncoding, allowLossyConversion: true)
        request.addValue("gzip", forHTTPHeaderField: "Accept-Encoding")

        let session = NSURLSession.sharedSession()
        let task = session.dataTaskWithRequest(request, completionHandler: {data, response, error -> Void in
            // debugging

            let strData = NSString(data: data, encoding: NSUTF8StringEncoding)
            println("Body: \(strData)\n\n")

            let xml = SWXMLHash.parse(data)
            if xml["soap:Envelope"].element != nil {
                // parse all data here
                var balance = Dictionary<String, AnyObject>()

                if let _ = xml["soap:Envelope"]["soap:Body"][“planDescription"].element {

                    // <..>

                    // get details
                    self.startDetailsRequest(forPhoneNumber: phoneNumber, balance: balance, completionHandler: completionHandler)

                } else {
                    println("Error: XML structure has been changed")
                    completionHandler(phoneNumber: phoneNumber, data: nil, errorMessage: "App is out of date. Please update.")
                }

            } else {
                println("Error: Internet connection failed")
                completionHandler(phoneNumber: phoneNumber, data: nil, errorMessage: "Error: Internet connection failed")
            }

        })

        task.resume()
    }

    private func startDetailsRequest(forPhoneNumber phoneNumber: String, var balance: Dictionary<String, AnyObject>, completionHandler: (phoneNumber: String, data: Dictionary<String, AnyObject>?, errorMessage: String?) -> Void) {
        let today = dateFormatter.stringFromDate(NSDate())
        let envelope = "<soapenv:Envelope xmlns:soapenv='http://schemas.xmlsoap.org/soap/envelope/'> ... </soapenv:Envelope>"

        let request = NSMutableURLRequest(URL: NSURL(string: WebserviceConstants.WebserviceURL)!)
        request.HTTPMethod = "POST"
        request.HTTPBody = envelope.dataUsingEncoding(NSUTF8StringEncoding, allowLossyConversion: true)
        request.addValue("gzip", forHTTPHeaderField: "Accept-Encoding")

        let session = NSURLSession.sharedSession()
        let task = session.dataTaskWithRequest(request, completionHandler: {[weak self] data, response, error -> Void in
            // debugging
            println("Response: \(response)")
            let strData = NSString(data: data, encoding: NSUTF8StringEncoding)
            println("Body: \(strData)\n\n")

            let xml = SWXMLHash.parse(data)
            if let planType = xml["soap:Envelope"]["soap:Body"]["ns1:out"]["isPostpagoOrHybrid"].element {
                /// <...>

                completionHandler(phoneNumber: phoneNumber, data: balance, errorMessage: nil)

            } else {
                if let _ = xml["soap:Envelope"].element {
                    println("Error: XML structure has been changed")
                    completionHandler(phoneNumber: phoneNumber, data: nil, errorMessage: "App is out of date. Please update.")
                } else {
                    println("Error: Couldn't connect to Iusacell server")
                    completionHandler(phoneNumber: phoneNumber, data: nil, errorMessage: "Error: Internet connection failed")
                }
            }

        })

        task.resume()
    }

}
drmohundro commented 9 years ago

Thanks! Give me some time to take a look at it and see if I can figure out what's going on.

drmohundro commented 9 years ago

I'm still unable to reproduce this on my own. When I call parse multiple times, it always seems to work.

Here are a few questions:

  1. In your code that you provided, do both methods result in blank data being returned? (i.e. both startBalanceRequest and startDetailsRequest or just startDetailsRequest?)
  2. Can you try passing in the strData instance that you retrieved from the NSData reference to parse to see if that works?

I think the issue has something to do with the data reference.

alusev commented 9 years ago
  1. What do you mean? The data is never empty. But if startBalanceRequest fails, then startDetailsRequest is never called. Sometimes the first function fails, sometimes the other.
  2. I am on it.
alusev commented 9 years ago

2 Now runs like clockwork. Thanks. What could be possibly wrong with my data? If I call the method within 4 hours, it is almost always the same response.

drmohundro commented 9 years ago

I noticed the second method used a weak reference for the data being passed in whereas the first method didn't use a weak reference. I don't know off hand why that would matter, but it was the only thing I spotted that looked a little different. The only guess I've got is that maybe it is getting cleaned up because it is a weak reference, but I have no idea at the moment.

I guessed that getting a string from it would bypass some of the weak reference issues, though.

It might be worthwhile to investigate weak reference issues in SWXMLHash.

alusev commented 9 years ago

I removed [weak self] a few days ago, it was a garbage that I left. The method is used to be a little bit different but I changed some code and forgot to remove weak self. However, the problem didn't go away until I changed parse’s parameter, like you suggested.

drmohundro commented 9 years ago

Interesting - so it still happened regardless of using a weak reference. I'll have to do some more research then.

drmohundro commented 8 years ago

I'm just going through old issues... I have yet to hear of any other issues like this one and I still haven't been able to reproduce it. Have you run into it again or had any similar issues? If not, I'd like to close this issue for the time being. Feel free to let me know either way, though.

(thanks for the report, too!)

alusev commented 6 years ago

It has been a long time since the last time I used the library. I ended up parsing the string.

GerdC commented 2 years ago

Since iOS 15 we have asynchronous sequences.

I suggested to Apple via Apple feedback to let us do something like

    let (bytes, response) = try await URLSession.shared.bytes(for: urlRequest)
    // bytes if of type URLSession.AsyncBytes 
    let parser = XMLParser(bytes) 
    parser.delegate = ... 
    parser.parse() 

So that the XMLParser instance can start parsing while the device is in the process of receiving data

If you think this is a good idea, it should help if more people make this suggestion to Apple.