How can I check that an NSData blob is valid as resumeData for an NSURLSessionDownloadTask?
Asked Answered
I

3

10

I have an app that's using background downloads with the new NSURLSession APIs. When a download cancels or fails in such a way that NSURLSessionDownloadTaskResumeData is provided, I store the data blob so that it can be resumed later. A very small amount of the time I am noticing a crash in the wild:

Fatal Exception: NSInvalidArgumentException
Invalid resume data for background download. Background downloads must use http or https and must download to an accessible file.

The error occurs here, where resumeData is the NSData blob and session is an instance of NSURLSession:

if (resumeData) {
    downloadTask = [session downloadTaskWithResumeData:resumeData];
    ...

The data is provided by the Apple APIs, is serialized, and is then deserialized at a later point in time. It may be corrupted, but it is never nil (as the if statement checks).

How can I check ahead of time that the resumeData is invalid so that I do not let the app crash?

Ina answered 20/2, 2014 at 0:46 Comment(0)
A
25

This is the workaround suggested by Apple:

- (BOOL)__isValidResumeData:(NSData *)data{
    if (!data || [data length] < 1) return NO;

    NSError *error;
    NSDictionary *resumeDictionary = [NSPropertyListSerialization propertyListWithData:data options:NSPropertyListImmutable format:NULL error:&error];
    if (!resumeDictionary || error) return NO;

    NSString *localFilePath = [resumeDictionary objectForKey:@"NSURLSessionResumeInfoLocalPath"];
    if ([localFilePath length] < 1) return NO;

    return [[NSFileManager defaultManager] fileExistsAtPath:localFilePath];
}

Edit (iOS 7.1 is not NDA'd anymore): I got this from a Twitter exchange with an Apple engineer, he suggested what to do, and I wrote the above implementation

Amphioxus answered 3/3, 2014 at 2:9 Comment(7)
Since 7.1 isn't under NDA anymore, could you point me to the source of the rumor? We got a lot of crashes under 7.1 now and the try/catch which worked fine under 7.0 doesn't work anymore.Atiptoe
The source was a Twitter exchange between myself and an Apple engineer. This was his last tweet: twitter.com/atnan/status/431571791799005184Amphioxus
Thanks for figuring out this workaround. This is a pretty poor API from Apple if it doesn't give you a way to validate the opaque data that it returns.Chalcis
This is the most helpful info I've found on this topic. Thanks! I'm encountering a different problem where the plist key @"NSURLSessionResumeBytesReceived" is returning @0, even when the temp file exists with an actual file size. As a test I've copied the dictionary with a modified @"NSURLSessionResumeBytesReceived" key (equal to the temp file size) and then reserialized back into an NSData object. Passing my modified data back to -downloadTaskWithResumeData: works without issue, but of course I'm hesitant to ship that. Any other info or experience you have related to this issue would helpful.Ocreate
For some reason my downloads won't ever resume. It is valid but I get an error "Invalid resume data for background download. Background downloads must use http or https and must download to an accessible file." even though the URL does have http at the beginning and is accessible. its super annoying.Olein
so anyone knows whats the solution for iOS 9 ?Fong
For those who is interested in this topic, the structure of resumeDictionary has changed on iOS9. NSURLSessionResumeInfoLocalPath is removed and makes it impossible to get the actual data. On iOS 10, this structure (somehow expectedly) changed again, becoming much more complicated. My choice is to simply returning YES on iOS 9 & 10.Posen
I
2

I have not found an answer to how to tell if the data is valid ahead of time.

However, I am presently working around the issue like so:

NSData *resumeData = ...;
NSURLRequest *originalURLRequest = ...;
NSURLSessionDownloadTask *downloadTask = nil;

@try {
    downloadTask = [session downloadTaskWithResumeData:resumeData];
}
@catch (NSException *exception) {
    if ([NSInvalidArgumentException isEqualToString:exception.name]) {
        downloadTask = [session downloadTaskWithRequest:originalURLRequest];
    } else {
        @throw exception; // only swallow NSInvalidArgumentException for resumeData
    }
}
Ina answered 24/2, 2014 at 17:58 Comment(0)
U
1

actually, the resume data is a plist file. it contains the follows key:

  • NSURLSessionDownloadURL
  • NSURLSessionResumeBytesReceived
  • NSURLSessionResumeCurrentRequest
  • NSURLSessionResumeEntityTag
  • NSURLSessionResumeInfoTempFileName
  • NSURLSessionResumeInfoVersion
  • NSURLSessionResumeOriginalRequest
  • NSURLSessionResumeServerDownloadDate so the steps u need to do are:

    1. check the data is a valid plist;
    2. check the plist have keys as above;
    3. check the temp file is exist;
Unquiet answered 20/11, 2015 at 1:29 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.