Can't fetch JSON due to NSInvalidArgumentException

Hey guys,


since NSURLConnection is deprecated in iOS 9, I'm trying to replace it with the recommended NSURLSession. When the completion handler of my NSURLSessionDataTask is getting called my app terminates due to an uncaught exception 'NSInvalidArgumentException' – the reason: 'data parameter is nil'. Of course a corrupt URL can be the reason for this error, but I've already checked that. Now, let me show you my code:

- (void)downloadItemsWithQuery:(NSString *)searchString {
    NSString *theQuery = [searchString uppercaseString];
    NSString *encodedString = [theQuery stringByAddingPercentEncodingWithAllowedCharacters:
               [NSCharacterSet characterSetWithCharactersInString:@"!*'();:@&=+$,/?%#[]"]];
    NSURL *theURL = [NSURL URLWithString:[NSString stringWithFormat:
                        @"http://alexpoets.com/VPlan/service.php?klasse=%@", encodedString]];
    NSURLSessionDataTask *dataTask = [[NSURLSession sharedSession] dataTaskWithURL:theURL
    completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
        _downloadedData = [[NSMutableData alloc] init];
        [_downloadedData appendData:data];       
    }];
    [dataTask resume];

I tried to debug the app – the app terminates in line 09. Does anyone of you have a clue why the data parameter is nil? I really appreciate any help you can provide.


Best wishes,


Alex

Answered by junkpile in 10306022

Looking at that code, I would suspect the "data is nil" error is coming from some other place, not from your NSURLSession completion block. If you are positive that's where the crash is, then if error is nil and data is nil, there must be an error HTTP response. Perhaps you're using the iOS 9 SDK and are getting tripped up by App Tranport Security. I do think you need to code more defensively there (check if data is nil, and check the HTTP response code).


Anyway here's one big thing that won't work with that code: NSURLSession is an asynchronous API. The completion block has not yet been called (most likely the request hasn't even gone out) by the time you hit that code that's attempting to parse the result. It's like you asked your roommate to go to the store and get some beer and put it in the fridge, then immediately went to the fridge and tried to pour out a beer. It's just not there yet. You need to defer all that parsing stuff until after the result comes in (i.e. put it in a separate method and call it from your completion block).

If the data parameter is nil, then error will be non-nil. What error is returned? Perhaps the same issue everyone is hitting with App Transport Security (need to add a plist entry to allow insecure HTTP connections)?

I failed to catch the error. First let me show you the tweaked code:

NSURLSessionDataTask *dataTask = [[NSURLSession sharedSession] dataTaskWithURL:theURL
    completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
        if (!error) {
        _downloadedData = [[NSMutableData alloc] init];
        [_downloadedData appendData:data];
        } else NSLog(@"%@", error);
    }];
    [dataTask resume];

If I set a breakpoint in line 03, I'm not able to read the error variable because the debugger immediately jumps in my main.m which looks like this:

int main(int argc, char * argv[]) {
    @autoreleasepool {
        return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
    }
}

In the main.m the program terminates in line 03. Do you have a clue what's the reason for this weird behavior? Did I make a mistake building the NSURLSessionDataTask? Thanks in advance,


Alex

If the debugger is not working right, a) file a bug, and b) just use old school logging: NSLog("Error: %@", error);


Even if there is no error, you may get a failure NSHTTPURLResponse. You should cast the response to NSHTTPURLResponse and check that the statusCode is success (typically 200, or maybe 200 through 299 depending how standards compliant you plan to be). If you get a 404 or something, error will still be nil.

I tried to use 'old school' logging:

if (!error) { 
        _downloadedData = [[NSMutableData alloc] init]; 
        [_downloadedData appendData:data]; 
        } else NSLog(@"%@", error);

The proplem is that before the else statement can be executed the debugger jumps in the main.m. As the result, it's just not possible to read the error variable. 😟 Neither it is possible to work with the NSURLResponse. Is there a typical reason why the debugger is jumping to the main.m?


Alex

Hmm not entirely sure then. Maybe we're looking at a red herring. What error messages are printed in the console? Do you have an exception breakpoint set (so it will break where the exception is thrown, rather than where it is caught in main.m)?

OK, thanks to your hint with the exception breakpoint I was able to read the error variable: Surprisingly it's nil. This caused me getting entirely confused. 😕 Anyway, in the console there is the NSException error printed

Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: 'data parameter is nil'

as well as a bunch of throw call stack annotations. I have already browsed them, but they didn't gimme a hint about the occured error. Clueless as I am, I assume that your red herring theory proves to be true. Possibly I forgot to set some delegates ... Would be absolutely cool if you have a look at the whole method. Maybe everything gets clearer, then.

- (void)downloadItemsWithQuery:(NSString *)searchString {
    // Create the customized URL
    NSString *theQuery = [searchString uppercaseString];
    NSString *encodedString = [theQuery stringByAddingPercentEncodingWithAllowedCharacters:
               [NSCharacterSet characterSetWithCharactersInString:@"!*'();:@&=+$,/?%#[]"]];
    NSURL *theURL = [NSURL URLWithString:[NSString stringWithFormat:
                        @"http://alexpoets.com/VPlan/service.php?klasse=%@", encodedString]];
   // Create the session to fetch the JSON
    NSURLSession *session = [NSURLSession sharedSession];
    [[session dataTaskWithURL:theURL
            completionHandler:^(NSData *data,
                                NSURLResponse *response,
                                NSError *error) {
                // handle response
                if (!error) {
                    // Initialize the data object
                    _downloadedData = [[NSMutableData alloc] init];
                    // Append the newly downloaded data
                    [_downloadedData appendData:data];
                } else NSLog(@"%@", error);
     
            }] resume];

    // Create an array to store the data
    NSMutableArray *_storedData = [[NSMutableArray alloc] init];

    // Parse the JSON that came in
    NSError *error;
    NSArray *jsonArray = [NSJSONSerialization JSONObjectWithData:_downloadedData
                              options:NSJSONReadingAllowFragments error:&error];

    // Loop through JSON data, create new objects and add them to the array
    for (int i = 0; i < jsonArray.count; i++)
    {
        NSDictionary *jsonElement = jsonArray[i];

        // Create new objects and set its props to JsonElement properties
        Qualifikationsphase *newQualifikationsphase = [[Qualifikationsphase alloc] init];
        newQualifikationsphase.tag = jsonElement[@"Tag"];
        newQualifikationsphase.art = jsonElement[@"Art"];
        newQualifikationsphase.klasse = jsonElement[@"Klasse"];
        newQualifikationsphase.stunde = jsonElement[@"Stunde"];
        newQualifikationsphase.vertretung = jsonElement[@"Vertretung"];
        newQualifikationsphase.fach = jsonElement[@"Fach"];
        newQualifikationsphase.raum = jsonElement[@"Raum"];
        newQualifikationsphase.bemerkung = jsonElement[@"Bemerkung"];
        newQualifikationsphase.identifier = jsonElement[@"Identifier"];

        // Add objects to the array
        [_storedData addObject:newQualifikationsphase];
    }

    // Ready to notify delegate that data is ready and pass back items
    if (self.delegate) [self.delegate itemsDownloaded:_storedData];
}

I'm so sorry for this bunch of code!!! 😊 It's my model (MVC pattern), BTW. I call this method in the controller, before the view is loaded. In the header file of my model class I set the following delegate:

#import <Foundation/Foundation.h>
@protocol HomeModelProtocol <NSObject>
- (void)itemsDownloaded:(NSArray *)items;
@end
@interface HomeModel : NSObject <NSURLSessionDataDelegate>
@property (nonatomic, weak) id<HomeModelProtocol>
delegate;
- (void)downloadItemsWithQuery:(NSString *)searchString;
@end

When the delegate is notified that the data items are ready to get passed back, this method in my controller is called:

- (void)itemsDownloaded:(NSArray *)items {
    // This delegate method will get called when the items are finished downloading

    // Set the downloaded items to the array
    _feedItems = items;

    // Reload the table view
    [self.listTableView reloadData];
}


Possibly I made a mistake concerning the delegates or rather the MVC pattern. I am SO grateful for any help you can provide!!! 🙂


Kind regards,


Alex

Accepted Answer

Looking at that code, I would suspect the "data is nil" error is coming from some other place, not from your NSURLSession completion block. If you are positive that's where the crash is, then if error is nil and data is nil, there must be an error HTTP response. Perhaps you're using the iOS 9 SDK and are getting tripped up by App Tranport Security. I do think you need to code more defensively there (check if data is nil, and check the HTTP response code).


Anyway here's one big thing that won't work with that code: NSURLSession is an asynchronous API. The completion block has not yet been called (most likely the request hasn't even gone out) by the time you hit that code that's attempting to parse the result. It's like you asked your roommate to go to the store and get some beer and put it in the fridge, then immediately went to the fridge and tried to pour out a beer. It's just not there yet. You need to defer all that parsing stuff until after the result comes in (i.e. put it in a separate method and call it from your completion block).

Hey, thank you ever so much for the great explanation!!! 🙂 I didn't know that NSURLSession is an asynchronous API. 😊 We're a step further; my app downloads the data as expected. There are some other problems, though: The app runs unstably now, sometimes it crashes, sometimes it doesn't. Furthermore the app is as slow as molasses in January. Also some weird display errors occur. It seems that due to the asynchronous API the table view already tries to reload by the time the data isn't loaded completely. Of course I was trying to debug the code, but how to debug when the app is running, actually? As I mentioned, the app crashes sometimes, though. Then it terminates as a result of an EXC_BAD_ACCESS, an URL timeout problem or a 'XPC connection interrupted'. But I think that these are just a red herring. The 'real problem' propably is due to my use of NSURLSession's asynchronous API; I assume that I made a mistake when notifying the delegate that the data is ready to be passed back ... A shortened version of my code now looks like this:

@interface HomeModel()
{
    NSMutableData *_downloadedData;
}
@end

@implementation HomeModel

- (void)downloadItemsWithQuery:(NSString *)searchString {
    // create the customized URL
    NSString *theQuery = [searchString uppercaseString];
    NSString *encodedString = [theQuery stringByAddingPercentEncodingWithAllowedCharacters:
    [NSCharacterSet characterSetWithCharactersInString:@"!*'();:@&=+$,/?%#[]"]];
    NSURL *theURL = [NSURL URLWithString:[NSString
    stringWithFormat:@"http://alexpoets.com/VPlan/service.php?klasse=%@", encodedString]];

    NSURLSession *session = [NSURLSession sharedSession];
    [[session dataTaskWithURL:theURL
    completionHandler:^(NSData * __nullable data, NSURLResponse * __nullable response, NSError * __nullable error) {
        // handle response
        if (!error) {
            // initialize the data object
            _downloadedData = [[NSMutableData alloc] init];
            // append the newly downloaded data
            [_downloadedData appendData:data];
            // call the parsing method
            [self parseJSON];
        } else NSLog(@"%@", error);
    }] resume];
}

- (void)parseJSON {
    // ...
    // some parsing stuff here
    // ...

    // ready to notify delegate that data is ready and pass back items
    if (self.delegate) [self.delegate itemsDownloaded:_storedData];
}
@end

Is there anything which could've caused this weird behavior? Which delegates do I have to declare in the header? ATM, I just declared the 'NSURLSessionDataDelegate' protocol. Should I use the '- URLSession:task:didCompleteWithError:' method instead of declaring my own method 'parseJSON'? Sorry for the chunk of questions ... 😊 Thank you SO much for your help!!!!! 🙂


Kind regards,


Alex

You need to update your UI on the main thread.


Running NSURLSession completion handler on main thread

Thanks, that worked. I had to dispatch the parsing stuff on the main thread. 🙂 Thank you ever so much for the help and the great explanations!!! You really helped me out! 🙂


Kind regards,


Alex

You're welcome. To be specific, for the best user experience you should do all the heavy lifting (the actual parsing) on the background thread, then dispatch *only* the model update and UI update (self.myModel = theDataIJustParsed; [self.tableView reloadData];) to the main thread.

Hmmm, I'm getting something similar. The response object is empty on a 200 when it shouldn't be. Running the exact same requsts via curl are completely healthy. This only regressed when running ios8.3 app on ios9.0.


This may be related

https://github.com/AFNetworking/AFNetworking/issues/2783

Thanks for the hint, I changed that. 🙂

Can't fetch JSON due to NSInvalidArgumentException
 
 
Q