16
votes

I am trying to upload a video / image file using- (NSURLSessionUploadTask *)uploadTaskWithRequest:(NSURLRequest *)request fromFile:(NSURL *)fileURL; method using multi-part form data. But somehow i am not able to upload the file and i am getting "stream ended unexpectedly" error.

Requirements

  1. Upload a video / image file to server
  2. App should support background uploads (Continue the upload process even after app goes into background)
  3. Server expects the data to be sent using multi-part form data.

Methods / API's used to achieve this

  1. NSURLSession background session API (Complete code listed below)

    2.- (NSURLSessionUploadTask *)uploadTaskWithRequest:(NSURLRequest *)request fromFile:(NSURL *)fileURL

Challenges / Problems being faced

  1. facing "stream ended unexpectedly" error every time I am using this API for upload process

Points to be noted

  1. The upload is getting successful with the same code if i am use NSURLConnection instead of NSURLSession.

  2. NSURLSession background upload process expects the file location (NSURL) as parameter, does not accept NSData. It does not allow us to convert the file to NSData before uploading, i.e, we can not add NSData to file body.

Need help on following points

  1. Is there any mistake in the multipart formdata body that is being formed (note - The same code is working with NSURLConnection)

  2. Where am i going wrong in my approach?

  3. Do we need to make any changes at the server level to support NSURLSession backgroundSession uploads? (in data parsing or something else?)

    Here is the code that is being used for uploading a file

NSString *BoundaryConstant = @"----------V2ymHFg03ehbqgZCaKO6jy";

    // string constant for the post parameter 'file'. My server uses this name: `file`. Your's may differ
    NSString* FileParamConstant = @"file";

    // the server url to which the image (or video) is uploaded. Use your server url here

    url=[NSURL URLWithString:[NSString stringWithFormat:@"%@%@%d",baseURL,@"posts/post/update/",createPostObject.PostID]];    


    // create request
    NSMutableURLRequest *request = [[NSMutableURLRequest alloc] init];
    [request setCachePolicy:NSURLRequestReloadIgnoringLocalCacheData];
    [request setHTTPShouldHandleCookies:NO];
    [request setTimeoutInterval:120];
    [request setHTTPMethod:@"POST"];
    [request addValue:@"multipart/form-data" forHTTPHeaderField:@"Content-Type"];

    [request setURL:url];

    // set Content-Type in HTTP header
    NSString *contentType = [NSString stringWithFormat:@"multipart/form-data; boundary=%@", BoundaryConstant];
    [request setValue:contentType forHTTPHeaderField: @"Content-Type"];

    if([[NSUserDefaults standardUserDefaults] objectForKey:@"accessToken"]){

        [request setValue:[[NSUserDefaults standardUserDefaults] objectForKey:@"accessToken"] forHTTPHeaderField:AccessTokenKey];

    }

    // post body
    NSMutableData *body = [NSMutableData data];

    // add params (all params are strings)
    for (NSString *param in self.postParams) {

        NSLog(@"param is %@",param);

        [body appendData:[[NSString stringWithFormat:@"--%@\r\n", BoundaryConstant] dataUsingEncoding:NSUTF8StringEncoding]];
        [body appendData:[[NSString stringWithFormat:@"Content-Disposition: form-data; name=\"%@\"\r\n\r\n", param]             dataUsingEncoding:NSUTF8StringEncoding]];
        [body appendData:[[NSString stringWithFormat:@"%@\r\n", [self.postParams objectForKey:param]] dataUsingEncoding:NSUTF8StringEncoding]];
    }

    // add video file name to body

        [body appendData:[[NSString stringWithFormat:@"--%@\r\n", BoundaryConstant] dataUsingEncoding:NSUTF8StringEncoding]];
        [body appendData:[[NSString stringWithFormat:@"Content-Disposition: form-data; name=\"%@\"; filename=\"file.mp4\"\r\n", FileParamConstant] dataUsingEncoding:NSUTF8StringEncoding]];
        [body appendData:[[NSString stringWithString:@"Content-Type: video/mp4\r\n\r\n"] dataUsingEncoding:NSUTF8StringEncoding]];
      //  [body appendData:self.dataToPost];
        [body appendData:[[NSString stringWithFormat:@"\r\n"] dataUsingEncoding:NSUTF8StringEncoding]];

        [body appendData:[[NSString stringWithFormat:@"--%@--\r\n", BoundaryConstant] dataUsingEncoding:NSUTF8StringEncoding]];



    // setting the body of the post to the request
    [request setHTTPBody:body];

    // set the content-length
    NSString *postLength = [NSString stringWithFormat:@"%lu", (unsigned long)[body length]];
    [request setValue:postLength forHTTPHeaderField:@"Content-Length"];

    NSLog(@"Request body %@", [[NSString alloc] initWithData:[request HTTPBody] encoding:NSUTF8StringEncoding]);

    NSURLSessionConfiguration * backgroundConfig = [NSURLSessionConfiguration backgroundSessionConfigurationWithIdentifier:@"backgroundtask1"];

    NSURLSession *backgroundSeesion = [NSURLSession sessionWithConfiguration: backgroundConfig delegate:self delegateQueue: [NSOperationQueue mainQueue]];


    NSURLSessionUploadTask *uploadTask = [backgroundSeesion uploadTaskWithRequest:request fromFile:self.videoUrl];
    [uploadTask resume];
2
You seem to set "Content-Type" twice. Not sure how that works out. Not sure about the rest. I do something similar, but create the upload-buffer separate (in a c++ function) and just add the hole buffer. So, I don't have to much more to contribute with.Aslak Berby

2 Answers

14
votes

You aren't uploading what you think you are. Your intent is for the body data to be uploaded as-is. Instead, when you call uploadTaskWithRequest:fromFile:, that method effectively nils out any HTTPBody or HTTPBodyStream values in the request and replaces them with the contents of the URL that you passed in via the fromFile: parameter.

So unless you're writing that block of form-encoded body data to that file URL somewhere else, you're uploading the file by itself instead of the multipart form data.

You need to tweak your code to write the form data out to a file instead of storing it in HTTPBody, then pass the URL of that file to the fromFile: parameter.

6
votes

To prevent wasting time dealing with it.

The complete snippet based on @dgatwood answer

private func http(request: URLRequest){
        let configuration = URLSessionConfiguration.default
        let session = URLSession(configuration: configuration, delegate: self, delegateQueue: .main)
        /*Tweaking*/
        let task = session.uploadTask(with: request, from: request.httpBody!)
        task.resume()
    }

And.. do not forget to add the Headers on request object like

request.setValue("multipart/form-data; boundary=\(yourboundary)", forHTTPHeaderField: "Content-Type")