1

Here is my REST API for uploading file-

@api.route('/update_profile_picture', methods=['POST'])
def update_profile_picture():

    if 'file' in request.files:
        image_file = request.files['file']
    else:
    return jsonify({'response': None, 'error' : 'NO File found in request.'})

    filename = secure_filename(image_file.filename)
    image_path = os.path.join(app.config['UPLOAD_FOLDER'], filename)
    image_file.save(image_path)

    try:
        current_user.image = filename
        db.session.commit()
    except Exception as e:
        return jsonify({'response': None, 'error' : str(e)})

    return jsonify({'response': ['{} profile picture update successful'.format(filename)], 'error': None})

The above code works fine as I tested with postman but in postman I can set a file object. However, when I try to upload from iOS app, it gives me the error-

NO File found in request

Here is my swift code to upload image-

struct ImageFile {
    let fileName : String
    let data: Data
    let mimeType: String
    
    init?(withImage image: UIImage, andFileName fileName: String) {
        self.mimeType = "image/jpeg"
        self.fileName = fileName
        guard let data = image.jpegData(compressionQuality: 1.0) else {
            return nil
        }
        self.data = data
    }
}

class FileLoadingManager{
    
    static let sharedInstance = FileLoadingManager()
    private init(){}
    
    let utilityClaas = Utility()
    
    func uploadFile(atURL urlString: String, image: ImageFile, completed:@escaping(Result<NetworkResponse<String>, NetworkError>)->()){
        
        guard let url = URL(string: urlString) else{
            return completed(.failure(.invalidURL))
        }
        
        var httpBody =  Data()
        let boundary = self.getBoundary()
    
        let lineBreak = "\r\n"
        let contentType = "multipart/form-data; boundary = --\(boundary)"
   
         httpBody.append("--\(boundary + lineBreak)")
         httpBody.append("Content-Disposition: form-data; name = \"file\"; \(lineBreak)")
         httpBody.append("Content-Type: \(image.mimeType + lineBreak + lineBreak)")
         httpBody.append(image.data)
         httpBody.append(lineBreak)
         httpBody.append("--\(boundary)--")
        
        let requestManager = NetworkRequest(withURL: url, httpBody: httpBody, contentType: contentType, andMethod: "POST")
        let urlRequest = requestManager.urlRequest()
        
        let dataTask = URLSession.shared.dataTask(with: urlRequest) {  (data, response, error) in
            if let error = error as? NetworkError{
                completed(.failure(error))
                return
            }
            if let response = response as? HTTPURLResponse{
                if response.statusCode < 200 || response.statusCode > 299{
                    completed(.failure(self.utilityClaas.getNetworkError(from: response)))
                    return
                }
            }

            guard let responseData = data else{
                completed(.failure(NetworkError.invalidData))
                return
            }

            do{
                let jsonResponse = try JSONDecoder().decode(NetworkResponse<String>.self, from: responseData)
                completed(.success(jsonResponse))
            }catch{
                completed(.failure(NetworkError.decodingFailed))
            }
        }
        dataTask.resume()
    }
    
    private func boundary()->String{
        return "--\(NSUUID().uuidString)"
    }
}

extension Data{
    mutating func append(_ string: String) {
        if let data = string.data(using: .utf8){
            self.append(data)
        }
    }
}

Also here is the NetworkRequest struct-

class NetworkRequest{
    
    var url: URL
    var httpBody: Data?
    var httpMethod: String
    var contentType = "application/json"
   
    
    init(withURL url:URL, httpBody body:Data, contentType type:String?, andMethod method:String) {
        self.url = url
        self.httpBody = body
        self.httpMethod = method
        if let contentType = type{
            self.contentType = contentType
        }
    }
    
    func urlRequest()->URLRequest{
        var request = URLRequest(url: self.url)
        
        request.addValue(contentType, forHTTPHeaderField: "Content-Type")
        request.httpBody = self.httpBody
        request.httpMethod = self.httpMethod
        return request
    }
    
}

In The ImageLoaderViewController, an image is selected to be sent to be uploaded.

class ImageLoaderViewController: UIViewController {
    
    @IBOutlet weak var selectedImageView: UIImageView!
       
    override func viewDidLoad() {
        super.viewDidLoad()
    }
   
    @IBAction func selectImage(){
        if selectedImageView.image != nil{
            selectedImageView.image = nil
        }
        let imagePicker = UIImagePickerController()
        imagePicker.sourceType = .photoLibrary
        imagePicker.delegate = self
        self.present(imagePicker, animated: true, completion: nil)
    }

    @IBAction func uploadImageToServer(){
        if let image = imageFile{
            DataProvider.sharedInstance.uploadPicture(image) { (msg, error) in
                if let error = error{
                    print(error)
                }
                else{
                    print(msg!)
                }
            }
        }
    }
   func completedWithImage(_ image: UIImage) -> Void {
        imageFile = ImageFile(withImage: image, andFileName: "test")
    }
}
extension ImageLoaderViewController: UIImagePickerControllerDelegate, UINavigationControllerDelegate{
    
    func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {
        if let image = info[.originalImage] as? UIImage{
            picker.dismiss(animated: true) {
                self.selectedImageView.image = image
                self.completedWithImage(image)
            }
        }
        picker.dismiss(animated: true, completion: nil)
    }
    
    func imagePickerControllerDidCancel(_ picker: UIImagePickerController) {
        picker.dismiss(animated: true, completion: nil)
    }
}
Natasha
  • 6,651
  • 3
  • 36
  • 58
  • 3
    Umm... That's not Swift. You should remove the `[swift]` tag from your question. – Duncan C Dec 15 '20 at 18:22
  • 1
    "I am trying to use URLSession." And do you have a Swift code? Do you know that POSTMAN can generate Swift code for your request? Not beautiful code, but code you might use/get inspired with? – Larme Dec 15 '20 at 18:30
  • jumping to wrong tag can be frustrating .. so please remove [swift] – zeytin Dec 15 '20 at 20:12
  • I think the author wants in the end Swift code, but didn't show any effort, just show his/her server code in another language. – Larme Dec 16 '20 at 09:48
  • I want some help or hints for resource that tells me how to write iOS client-end code to send image using URLSession. – Natasha Dec 16 '20 at 11:19
  • After fixing the boundary, it looks pretty good to me, but there might be subtle issues with the format of a multipart request. Ensure a) that in your request, after the HTTP headers, an extra CRLF is set. Otherwise add a CRLF (`lineBreak`) immediately before you add the initial boundary. b) remove any WS between tokens, which are not explicitly allowed in the RFCs (rfc2183, ...). Ex: `name = \"\(file)\"` -> `name=\"\(file)\"`, and other occurrences. Server should handle this gracefully if it's not ambiguous, but they may be strict. The last CRLF (after the closing boundary) is not needed. – CouchDeveloper Dec 20 '20 at 18:40

4 Answers4

1

The mistake is that you call boundary() function each time in your code that generates you new UUID but the resource must have a single one. So just generate UUID for your resource once and then insert this value where you need:

...
let boundary = boundary()
let contentType = "multipart/form-data; boundary = \(boundary)"
...
iUrii
  • 11,742
  • 1
  • 33
  • 48
  • I tried your suggestion but that didn't work either. – Natasha Dec 18 '20 at 13:04
  • @Natasha You must use the same boundary in the same request! So, better define it as some let value and use that in the multipart data, as iUrii suggested ;) It's OK to have a new boundary for every request. It will be defined in the Content-Type request header. It must be a sequence of chars which does not occur inside the payload. The specified boundary must then be used in the multipart as defined in the Content-Type request header. – CouchDeveloper Dec 20 '20 at 18:44
  • Yes, I exactly did so but it didn't work. I updated my post with the suggested update. – Natasha Dec 21 '20 at 11:56
0

Setting up a multipart form-data content can be tricky. Especially there can be subtle errors when combining the many parts of the request body.

Content-Type request header value:

let contentType = "multipart/form-data; boundary = --\(boundary)"

Here, the boundary parameter should not be preceded with the prefix "--". Also, remove any WS that are not explicitly allowed according the corresponding RFC. Furthermore, enclosing the boundary parameter in double quotes makes it more robust and never hurts:

let contentType = "multipart/form-data; boundary=\"\(boundary)\""

Initial body:

httpBody.append("--\(boundary + lineBreak)")

This is the start of the body. Before the body, the request headers are written into the body stream. Each header is completed with a CRLF, and after the last header another CRLF must be written. Well, I am pretty sure, URLRequest will ensure this. Nonetheless, it might be worth checking this with a tool that shows the characters written over the wire. Otherwise, add a preceding CRLF to the boundary (which conceptually belongs to the boundary anyway, and it does not hurt also):

httpBody.append("\(lineBreak)--\(boundary)\(lineBreak)")

Content-Disposition:

httpBody.append("Content-Disposition: form-data; name = \"file\"; \(lineBreak)")

Here, again you may remove the additional WS:

httpBody.append("Content-Disposition: form-data; name=\"file\"; \(lineBreak)")

Optionally, you may want to provide a filename parameter and a value. This is not mandatory, though.

Closing boundary There's no error here:

httpBody.append(lineBreak)
httpBody.append("--\(boundary)--")

But you might want to make it clear, that the preceding CRLF belongs to the boundary:

httpBody.append("\(lineBreak)--\(boundary)--")

Characters after the closing boundary will be ignored by the server.

Encoding

extension Data{
    mutating func append(_ string: String) {
        if let data = string.data(using: .utf8){
            self.append(data)
        }
    }
}

You cannot generally return utf8 encoded strings and embed this into the many different parts of a HTTP request body. Many parts of the HTTP protocol allow only a restricted set of characters. In many cases, UTF-8 is not allowed. You have to look-up the details in the corresponding RFCs - which is cumbersome, but also enlightened ;)

References:

RFC 7578, Definition of multipart/form-data

Community
  • 1
  • 1
CouchDeveloper
  • 18,174
  • 3
  • 45
  • 67
  • I updated with all of your suggestions but still no luck. – Natasha Dec 22 '20 at 11:00
  • So far, we discovered a few errors which actually prevented to send a valid request. I am not sure if, we have found every issue yet. You may now want to log the URLRequest (in a Unit Test) once you have created it. Also print out the UTF-8 decoded body and check if it is a valid multipart/form-data body. You can check this in a Unit test. Possibly also use a tool like Charles Proxy to check if this tool recognises the request. If this is OK, you need to check your server. Note, that we have no idea what your server expects and "NO File found in request" is your custom error handling. – CouchDeveloper Dec 22 '20 at 14:11
  • Also, you may try a mock server which can handle multipart/form-data. Send a short file to check your client approach. A side note: networking can be tricky at this level, so I am not surprised about arising issues :) Most developers use client libraries when they have to send a multipart/form-data. ;) – CouchDeveloper Dec 22 '20 at 14:17
0

This is my way to upload a file from IOS Client using multipart form , with the help of Alamofire library

let url = "url here"
let headers: HTTPHeaders = [
        "Authorization": "Bearer Token Here",
        "Accept": "application/x-www-form-urlencoded"
    ]
AF.upload(multipartFormData: { (multipartFormData) in
            multipartFormData.append(imageData, withName: "image" ,fileName: "image.png" , mimeType: "image/png")
        }, to: url, method: .post ,headers: headers).validate(statusCode: 200..<300).response { }
Ouail Bellal
  • 1,554
  • 13
  • 27
0

Here is nice example of Multipart. I think it could be something wrong with building multipart:

let body = NSMutableData()
        
        if parameters != nil {
            for (key, value) in parameters! {
                body.appendString("--\(boundary)\r\n")
                body.appendString("Content-Disposition: form-data; name=\"\(key)\"\r\n\r\n")
                body.appendString("\(value)\r\n")
            }
        }
        
        if fileURLs != nil {
            if fileKeyName == nil {
                throw NSError(domain: NSBundle.mainBundle().bundleIdentifier ?? "NSURLSession+Multipart", code: -1, userInfo: [NSLocalizedDescriptionKey: "If fileURLs supplied, fileKeyName must not be nil"])
            }
            
            for fileURL in fileURLs! {
                let filename = fileURL.lastPathComponent
                guard let data = NSData(contentsOfURL: fileURL) else {
                    throw NSError(domain: NSBundle.mainBundle().bundleIdentifier ?? "NSURLSession+Multipart", code: -1, userInfo: [NSLocalizedDescriptionKey: "Unable to open \(fileURL.path)"])
                }
                
                let mimetype = NSURLSession.mimeTypeForPath(fileURL.path!)
                
                body.appendString("--\(boundary)\r\n")
                body.appendString("Content-Disposition: form-data; name=\"\(fileKeyName!)\"; filename=\"\(filename!)\"\r\n")
                body.appendString("Content-Type: \(mimetype)\r\n\r\n")
                body.appendData(data)
                body.appendString("\r\n")
            }
        }
        
        body.appendString("--\(boundary)--\r\n")
        return body