Search code examples
swiftsoapalamofiremultiparturlsession

Parse a multipart SOAP response in Swift


I'm trying to download a file from a soap service and I'm getting the following after a successful request

the response header

multipart/related; type="application/xop+xml"; boundary="uuid:917b60a9-3089-43ad-a8c2-b4a3c62db98c"; start="<root.message@cxf.apache.org>"; start-info="text/xml"

response body

--uuid:0a679f64-0753-44fe-b627-2267b5b72b1d
Content-Type: application/xop+xml; charset=UTF-8; type="text/xml"
Content-Transfer-Encoding: binary
Content-ID: <root.message@cxf.apache.org>

<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"><soap:Body><ns2:leseDokumentResponse xmlns:ns2="http://webservice/"><return><status>OK</status><dokument><xop:Include xmlns:xop="http://www.w3.org/2004/08/xop/include" href="cid:c58d3315-9cb6-413a-98a1-5a29671cfdb6-41@cxf.apache.org"/></dokument></return></ns2:leseDokumentResponse></soap:Body></soap:Envelope>
--uuid:0a679f64-0753-44fe-b627-2267b5b72b1d
Content-Type: application/octet-stream
Content-Transfer-Encoding: binary
Content-ID: <c58d3315-9cb6-413a-98a1-5a29671cfdb6-41@cxf.apache.org>

... binary data ...
--uuid:0a679f64-0753-44fe-b627-2267b5b72b1d--

How can I parse the file binary data?

I tried these questions but nothing works Parsing http-multipart response Parse multipart response for image download in ios


Solution

  • 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.