Parse Multipart Response for Image Download in iOS

NSData Multi-part response from NSURLConnection

This looks like very plausible multipart response, I see what appears to be a valid JPEG (or at least the start of one) in the second part. This is extremely unusual to see multipart responses: We construct multipart requests in our iOS code all the time, but we don't handle multipart responses, generally.

Your Objective-C code, though is trying to load this as an image, and it's not a simple image. You actually have to parse the multipart response.

Before we drag you through the weeds of the ugly process of parsing this multipart response, are you stuck with this sort of response? I'd much rather see a simple JSON response with the image base-64 encoded. Or a JSON response with a URL for the image which can be downloaded separately.

It's going to take a lot of ugly code to parse this non-standard response, so the question is whether there's any chance to change the response.


It sounds like you're stuck with this format. In that case, to parse this response, you first have to get the boundary out of the Content-Type in the NSURLResponse headers. Then you can scan the NSData response payload for -- followed by that boundary string. Then you're going to have a couple of headers for each part, followed by a blank line, followed by the payload. The first payload is the JSON. The second is the image. You simple read in the bytes until you encounter the next \r\n followed by --, followed by the boundary. The whole thing is terminated with a -- followed by a boundary, followed immediately by another --.


For discussions on how multipart responses are constructed see the discussion of multipart responses in RFC 1341 (the MIME spec). Or see RFC 2388 (the multipart request created by HTML forms). Or see this example of how to create a multipart request in Objective-C. Or just google "create multipart request". Once you get your arms around how multipart responses are created, then the parsing of the multipart response will be fairly intuitive. But definitely make sure you grok multipart payloads before going too much further.

See Parse multipart response for image download in ios for some promising Objective-C code for parsing these sorts of responses.

Parse a multipart SOAP response in Swift

Parsing a multi-part SOAP response can be easily done by using MultipartKit

I created an extension on Alamofire's DataResponse to handle the response

import Alamofire
import MultipartKit
import Swime

extension DataResponse where Success == Data {

var boundary: String? {

let header = response?.allHeaderFields.first(where: {
$0.key as? String == "Content-Type"
})

let scanner = Scanner(string: header?.value as? String ?? "")

_ = scanner.scanUpToString("boundary=\"")
_ = scanner.scanString("boundary=\"")
let boundary = scanner.scanUpToString("\";")

return boundary
}

func multipartParts() -> [MultipartPart] {

guard let boundary = boundary else { return [] }

guard let data = try? result.get() else { return [] }

let parser = MultipartParser(boundary: boundary)

var parts: [MultipartPart] = []
var headers: HTTPHeaders = [:]
var body: Data = Data()

parser.onHeader = { (field, value) in
headers.replaceOrAdd(name: field, value: value)
}
parser.onBody = { new in
body.append(contentsOf: new.readableBytesView)
}
parser.onPartComplete = {
let part = MultipartPart(headers: headers, body: body)
headers = [:]
body = Data()
parts.append(part)
}

do {
try parser.execute(data)
} catch {
print(error.localizedDescription)
}

return parts
}

func parseMultipart() -> (message: String, files: [Data])? {

let parts = multipartParts()

let message = parts.first(where: { $0.id == "<root.message@cxf.apache.org>" })?.body.string ?? ""

let dataPart = parts.filter { $0.id != "<root.message@cxf.apache.org>" }

let files = dataPart.compactMap { Data(multipart: $0) }

return (message, files)
}
}

extension ByteBuffer {
var string: String {
return String(decoding: self.readableBytesView, as: UTF8.self)
}
}

extension MultipartPart {

/// Gets or sets the `name` attribute from the part's `"Content-ID"` header.
public var id: String? {
get { self.headers.first(name: "Content-ID") }
}
}

which then can be used on a data response like this:

let request = SessionManager.request(urlRequest).responseData(completionHandler: { response in

switch response.result {
case .failure(let error):
self.alert(error)

case .success:

guard let parts = response.parseMultipart() else { return }

DispatchQueue.main.async {

self.loadResponse(message: parts.message, files: parts.files)
}
}
})

the message id can be different so I would maybe suggest checking the type too

Hope this will help someone.



Related Topics



Leave a reply



Submit