unable to upload file using NSURLSession multi-part form data in iOS
G

2

17

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];
Guanabara answered 28/4, 2016 at 11:34 Comment(1)
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.Cacilia
P
15

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.

Perquisite answered 8/5, 2016 at 4:5 Comment(7)
1. Change fromFile: to fromData:, and pass the value of body.Perquisite
2. Uncomment // [body appendData:self.dataToPost]; and change self.dataToPost to [NSData dataWithContentsOfURL:self.videoURL].Perquisite
And now you have a complete code snippet. You can also optionally remove the [request setHTTPBody:body] line, because it doesn't do anything of value.Perquisite
fromData does not work for background uploadsSantasantacruz
The original question wasn't about background uploads. If you're trying to do uploads in a background session and you want to ensure that the upload continues even if the app gets suspended, just write the body data to a temporary file, create a task using that file as the data source, and don't forget to delete the temporary file when the upload finishes.Perquisite
I tried that.However, AWS still complains about missing values (like key) that should actually be in the body. Do you happen to know any code samples out there that actually work?Santasantacruz
Ok. I got it working. Actually my multipart form was invalid. Thanks.Santasantacruz
M
6

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")
Margarettmargaretta answered 24/1, 2017 at 4:37 Comment(1)
Where is the file?Inflow

© 2022 - 2024 — McMap. All rights reserved.