5

In my application, I'm downloading image from server as multipart content. In my response data I'm getting 2 parts: one is json content and other is downloaded file. The response is in following format.

--poa89012-3212-1232-9201-fdsakjkj921
Content-Type: application/json; charset=utf-8
Content-Disposition: inline; name=info

{
  //json content
}

--poa89012-3212-1232-9201-fdsakjkj921
Content-Disposition: file; name=file; filename=photo.png
Content-Type: application/octet-stream

// File data
˘íë77íí77Í¥2008:02:11 11:32:512008:02:1
------

I'm not able to handle this response which has 2 parts, when I tried to get the headers in didReceiveResponse: it gives the headers for the entire response whose content-type is multipart/mixed.Please show me the way to handle this response by splitting the json content and the file content.

smily
  • 357
  • 1
  • 7
  • 18
  • 1
    any specific reason for using multipart in response, it will be easy if you get image url in response, so you can download it asynchronously. – prasad Feb 28 '14 at 12:31
  • We are sending and receiving image in multipart only – smily Feb 28 '14 at 12:37
  • 1
    sending is fine but for receiving no one use multipart response, as user needs to wait until downloading of whole data, if you have used Facebook graph api, twitter api, you can check they all doing same. – prasad Feb 28 '14 at 14:10

3 Answers3

5

I also did have problems with http-multipart response. I wrote a category for NSData. Code below:

NSData+MultipartResponses.h

#import <Foundation/Foundation.h>

@interface NSData (MultipartResponses)

- (NSArray *)multipartArray;
- (NSDictionary *)multipartDictionary;

@end

NSData+MultipartResponses.m

#import "NSData+MultipartResponses.h"

@implementation NSData (MultipartResponses)

static NSMutableDictionary *parseHeaders(const char *headers)
{
   NSMutableDictionary *dict=[NSMutableDictionary dictionary];
   int max=strlen(headers);
   int start=0;
   int cursor=0;
   while(cursor<max)
   {
      while((headers[cursor]!=':')&&(headers[cursor]!='='))
      {
         cursor++;
      }
      NSString *key=[[NSString alloc] initWithBytes:(headers+start) length:(cursor-start) encoding:NSASCIIStringEncoding];
      cursor++;

      while(headers[cursor]==' ')
      {
         cursor++;
      }
      start=cursor;
      while(headers[cursor]&&(headers[cursor]!=';')&&((headers[cursor]!=13)||(headers[cursor+1]!=10)))
      {
         cursor++;
      }

      NSString *value;
      if((headers[start]=='"')&&(headers[cursor-1]=='"'))
      {
         value=[[NSString alloc] initWithBytes:(headers+start+1) length:(cursor-start-2) encoding:NSASCIIStringEncoding];
      }
      else
      {
         value=[[NSString alloc] initWithBytes:(headers+start) length:(cursor-start) encoding:NSASCIIStringEncoding];
      }
      [dict setObject:value forKey:key];

      if(headers[cursor]==';')
      {
         cursor++;
      }
      else
      {
         cursor+=2;
      }

      while(headers[cursor]==' ')
      {
         cursor++;
      }
      start=cursor;
   }
   return dict;
}

- (NSDictionary *)multipartDictionaryWithBoundary:(NSString *)boundary
{
   NSMutableDictionary *dict=[NSMutableDictionary dictionary];

   const char *bytes=(const char *)[self bytes];
   const char *pattern=[boundary cStringUsingEncoding:NSUTF8StringEncoding];

   int cursor=0;
   int start=0;
   int max=[self length];
   int keyNo=0;
   while(cursor<max)
   {
      if(bytes[cursor]==pattern[0])
      {
         int i;
         int patternLength=strlen(pattern);
         BOOL match=YES;
         for(i=0; i<patternLength; i++)
         {
            if(bytes[cursor+i]!=pattern[i])
            {
               match=NO;
               break;
            }
         }
         if(match)
         {
            if(start!=0)
            {
               int startOfHeaders=start+2;
               int cursor2=startOfHeaders;
               while((bytes[cursor2]!=(char)0x0d)||(bytes[cursor2+1]!=(char)0x0a)||(bytes[cursor2+2]!=(char)0x0d)||(bytes[cursor2+3]!=(char)0x0a))
               {
                  cursor2++;
                  if(cursor2+4==max)
                  {
                     break;
                  }
               }
               if(cursor2+4==max)
               {
                  break;
               }
               else
               {
                  int lengthOfHeaders=cursor2-startOfHeaders;
                  char *headers=(char *)malloc((lengthOfHeaders+1)*sizeof(char));
                  strncpy(headers, bytes+startOfHeaders, lengthOfHeaders);
                  headers[lengthOfHeaders]=0;

                  NSMutableDictionary *item=parseHeaders(headers);

                  int startOfData=cursor2+4;
                  int lengthOfData=cursor-startOfData-2;

                  if(([item valueForKey:@"Content-Type"]==nil)&&([item valueForKey:@"filename"]==nil))
                  {
                     NSString *string=[[NSString alloc] initWithBytes:(bytes+startOfData) length:lengthOfData encoding:NSUTF8StringEncoding];
                     keyNo++;
                     [dict setObject:string forKey:[NSString stringWithFormat:@"%d", keyNo]];
                  }
                  else
                  {
                     NSData *data=[NSData dataWithBytes:(bytes+startOfData) length:lengthOfData];
                     [item setObject:data forKey:@"data"];
                     keyNo++;
                     [dict setObject:item forKey:[NSString stringWithFormat:@"%d", keyNo]];
                  }
               }
            }
            cursor=cursor+patternLength-1;
            start=cursor+1;
         }
      }
      cursor++;
   }

   return dict;
}

- (NSArray *)multipartArray
{
   NSDictionary *dict=[self multipartDictionary];
   NSArray *keys=[[dict allKeys] sortedArrayUsingSelector:@selector(localizedStandardCompare:)];
   NSMutableArray *array=[NSMutableArray array];
   for(NSString *key in keys)
   {
      [array addObject:dict[key]];
   }
   return array;
}

- (NSDictionary *)multipartDictionary
{
   const char *bytes=(const char *)[self bytes];
   int cursor=0;
   int max=[self length];
   while(cursor<max)
   {
      if(bytes[cursor]==0x0d)
      {
         break;
      }
      else
      {
         cursor++;
      }
   }
   char *pattern=(char *)malloc((cursor+1)*sizeof(char));
   strncpy(pattern, bytes, cursor);
   pattern[cursor]=0x00;
   NSString *boundary=[[NSString alloc] initWithCString:pattern encoding:NSUTF8StringEncoding];
   free(pattern);
   return [self multipartDictionaryWithBoundary:boundary];
}

@end
Community
  • 1
  • 1
Darkngs
  • 6,381
  • 5
  • 25
  • 30
3

This is an old topic but agree with @possen ("This does not work so great if your parts consist of say UTF-8 and binary data because it tries to treat it as UTF-8, binary data will cause the initWithData function to fail. Instead, you need to stream the data in and handle each type separately for each encoding based upon the content-type.").

If this helps, this is a Swift implementation that handle binary image.

if let multiparts = responseData?.multipartArray(withBoundary: boundary) {
    for part in multiparts {
        if part.contentType == "application/json" {
            let a = try? JSONDecoder().decode(YOUR_DECODABLE_STRUCT.self, from: part.body)
        } else if part.contentType == "image/jpg" {
            let imageData = part.body
        }
    }                    
}
extension Data {
    
    func multipartArray(withBoundary boundary: String, key: String = "Content-Type:") -> [(contentType: String, body: Data)]? {
        func extractBody(_ data: Data) -> Data? {
            guard let startOfLine = key.data(using: .utf8) else { return nil }
            guard let endOfLine = "\r\n".data(using: .utf8) else { return nil }
            var result: Data? = nil
            var pos = data.startIndex
            while let r1 = data[pos...].range(of: startOfLine)
            {
                if let r2 = data[r1.upperBound...].range(of: endOfLine) {
                    pos = r2.upperBound
                }
            }
            
            if pos < data.endIndex {
                result = data[(pos+2)...]
            }
            return result
        }
        
        let multiparts = components(separatedBy: ("--" + boundary))
        var result: [(String, Data)]? = nil
        for part in multiparts
            .enumerated()
            .map({ index, data -> Data in
                if index == multiparts.count-1 {
                    return data.dropLast(2)
                } else {
                    return data
                }
            })
        {
            for contentTypeData in part.slices(between: key, and: "\r") {
                if let contentType = String(data: contentTypeData, encoding: .utf8),
                   let body = extractBody(part)
                {
                    if result == nil {
                        result = [(String, Data)]()
                    }
                    result?.append(
                        (contentType.trimmingCharacters(in: .whitespacesAndNewlines), body)
                    )
                } else {
                    continue
                }
            }
        }
        return result
    }
    
    func slices(between from: String, and to: String) -> [Data] {
        guard let from = from.data(using: .utf8) else { return [] }
        guard let to = to.data(using: .utf8) else { return [] }
        return slices(between: from, and: to)
    }
    
    func slices(between from: Data, and to: Data) -> [Data] {
        var chunks: [Data] = []
        var pos = startIndex
        while let r1 = self[pos...].range(of: from),
              let r2 = self[r1.upperBound...].range(of: to)
        {
            chunks.append(self[r1.upperBound..<r2.lowerBound])
            pos = r1.upperBound
        }
        return chunks
    }
    
    func components(separatedBy separator: String) -> [Data] {
        guard let separator = separator.data(using: .utf8)  else { return [] }
        return components(separatedBy: separator)
    }
    
    func components(separatedBy separator: Data) -> [Data] {
        var chunks: [Data] = []
        var pos = startIndex
        while let r = self[pos...].range(of: separator) {
            if r.lowerBound > pos {
                chunks.append(self[pos..<r.lowerBound])
            }
            pos = r.upperBound
        }
        if pos < endIndex {
            chunks.append(self[pos..<endIndex])
        }
        return chunks
    }
}

Paul Bancarel
  • 259
  • 1
  • 3
  • this is a very good swift answer to original problem (when considering a swift app is getting data from HTML multipart/form-data), should be adapted though to each users particular use case, for example can be used with
    take note of the extra form variable "boundary", and in the Paul's code above (withBoundary: boundary) would be in this case (withBoundary: "XXX")
    – hokkuk Mar 07 '21 at 20:41
1

For me, your code didn't work. Instead, I rewrote that code in pure objective-c: Take care that (my) boundaries in this code always have additional -- in front of the next boundary and a final boundary - those are stripped. An NSArray is returned with a NSDictionary for each part, containing the keys "headers" as NSDictionary and "body" as NSData

- (NSArray *)multipartArrayWithBoundary:(NSString *)boundary
{
    NSString *data = [[[NSString alloc] initWithData:self encoding:NSUTF8StringEncoding] autorelease];

    NSArray *multiparts = [data componentsSeparatedByString:[@"--" stringByAppendingString:boundary]]; // remove boundaries
    multiparts = [multiparts subarrayWithRange:NSMakeRange(1, [multiparts count]-2)]; // continued removing of boundaries

    NSMutableArray *toReturn = [NSMutableArray arrayWithCapacity:2];
    for(NSString *part in multiparts)
    {
        part = [part stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];
        NSArray *separated = [part componentsSeparatedByString:@"\n\n"];

        NSMutableDictionary *headers = [NSMutableDictionary dictionaryWithCapacity:3];
        for(NSString *headerLine in [[separated objectAtIndex:0] componentsSeparatedByString:@"\n"])
        {
            NSArray *keyVal = [headerLine componentsSeparatedByString:@":"];

            [headers setObject:[[keyVal objectAtIndex:1] stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]] forKey:[[keyVal objectAtIndex:0] stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]]];
        }

        [toReturn addObject:[NSDictionary dictionaryWithObjectsAndKeys:[[separated objectAtIndex:1] dataUsingEncoding:NSUTF8StringEncoding], @"body", headers, @"headers", nil]];
    }

    return toReturn;
}
smat88dd
  • 2,258
  • 2
  • 25
  • 38
  • This does not work so great if your parts consist of say UTF-8 and binary data because it tries to treat it as UTF-8, binary data will cause the initWithData function to fail. Instead, you need to stream the data in and handle each type separately for each encoding based upon the content-type. – possen Jul 14 '16 at 18:38
  • I agree with @possen. the parser is failing on the utf8 conversion. – LuAndre Nov 21 '16 at 15:44