Home > Back-end >  Set boundary on URLSession.uploadTask :with :fromFile
Set boundary on URLSession.uploadTask :with :fromFile

Time:11-21

I am trying to upload an image to a spring backend. It's supposed to work when in the background, therefore I can only use the session.uploadTask function My problem is that the backend is expecting me to set a Content-Type header. One crucial part is to define the boundary and use it accordingly in my request body, but how am I supposed to set my boundary on an Image?

Most tutorials I have seen do this with the session.uploadData function, which isn't available when you want to do the operation in the background. There I could simply append the boundary to the data.

To summarise: How can I use the header field boundary correctly when uploading images with uploadTask(with request: URLRequest, fromFile fileURL: URL)?

I am getting this error from spring:

org.springframework.web.multipart.MultipartException: Current request is not a multipart request

My Code:

let boundary = UUID().uuidString
// A background upload task must specify a file
var imageURLRequest = URLRequest(url: uploadURL)
imageURLRequest.httpMethod = "Post"
imageURLRequest.setValue("multipart/form-data; boundary=\(boundary)", forHTTPHeaderField: "Content-Type")
                
let imageTask = URLBackgroundSession.shared.uploadTask(with: imageURLRequest, fromFile: URL(fileURLWithPath: imagePath))
imageTask.resume()

CodePudding user response:

In those other examples you found (e.g., Upload image with parameters in Swift), they build a Data that conforms to a properly-formed multipart/form-data request and use that in the body of the request.

You will have to do the same here, except that rather than building a Data, you will create a temporary file, write all of this to that file, and then use that file in your uploadTask.


For example:

func uploadImage(from imageURL: URL, filePathKey: String, to uploadURL: URL) throws {
    let boundary = UUID().uuidString
    var imageURLRequest = URLRequest(url: uploadURL)
    imageURLRequest.httpMethod = "POST"
    imageURLRequest.setValue("multipart/form-data; boundary=\(boundary)", forHTTPHeaderField: "Content-Type")

    let folder = URL(filePath: NSTemporaryDirectory()).appending(path: "uploads")
    try FileManager.default.createDirectory(at: folder, withIntermediateDirectories: true)
    let fileURL = folder.appendingPathExtension(boundary)

    guard let outputStream = OutputStream(url: fileURL, append: false) else {
        throw OutputStream.OutputStreamError.unableToCreateFile(fileURL)
    }

    outputStream.open()
    try outputStream.write("--\(boundary)\r\n")
    try outputStream.write("Content-Disposition: form-data; name=\"\(filePathKey)\"; filename=\"\(imageURL.lastPathComponent)\"\r\n")
    try outputStream.write("Content-Type: \(imageURL.mimeType)\r\n\r\n")
    try outputStream.write(contentsOf: imageURL)
    try outputStream.write("\r\n")
    try outputStream.write("--\(boundary)--\r\n")
    outputStream.close()

    let imageTask = URLBackgroundSession.shared.uploadTask(with: imageURLRequest, fromFile: fileURL)
    imageTask.resume()
}

You should probably remove the temporary file in your urlSession(_:task:didCompleteWithError:).

FWIW, the above uses the following extensions to simplify the generation of the OutputStream:

extension OutputStream {
    enum OutputStreamError: Error {
        case stringConversionFailure
        case unableToCreateFile(URL)
        case bufferFailure
        case writeFailure
        case readFailure
    }

    /// Write `String` to `OutputStream`
    ///
    /// - parameter string:                The `String` to write.
    /// - parameter encoding:              The `String.Encoding` to use when writing the string. This will default to `.utf8`.
    /// - parameter allowLossyConversion:  Whether to permit lossy conversion when writing the string. Defaults to `false`.

    func write(_ string: String, encoding: String.Encoding = .utf8, allowLossyConversion: Bool = false) throws {
        guard let data = string.data(using: encoding, allowLossyConversion: allowLossyConversion) else {
            throw OutputStreamError.stringConversionFailure
        }
        try write(data)
    }

    /// Write `Data` to `OutputStream`
    ///
    /// - parameter data:                  The `Data` to write.

    func write(_ data: Data) throws {
        try data.withUnsafeBytes { (buffer: UnsafeRawBufferPointer) throws in
            guard var pointer = buffer.baseAddress?.assumingMemoryBound(to: UInt8.self) else {
                throw OutputStreamError.bufferFailure
            }

            var bytesRemaining = buffer.count

            while bytesRemaining > 0 {
                let bytesWritten = write(pointer, maxLength: bytesRemaining)
                if bytesWritten < 0 {
                    throw OutputStreamError.writeFailure
                }

                bytesRemaining -= bytesWritten
                pointer  = bytesWritten
            }
        }
    }

    /// Write contents of local `URL` to `OutputStream`
    ///
    /// - parameter fileURL:                  The `URL` of the file to written to this output stream.

    func write(contentsOf fileURL: URL) throws {
        guard let inputStream = InputStream(url: fileURL) else {
            throw OutputStreamError.readFailure
        }

        inputStream.open()

        let bufferSize = 1024
        let buffer = UnsafeMutablePointer<UInt8>.allocate(capacity: bufferSize)
        defer { buffer.deallocate() }

        while inputStream.hasBytesAvailable {
            let length = inputStream.read(buffer, maxLength: bufferSize)
            if length > 0 {
                write(buffer, maxLength: length)
            }
        }

        inputStream.close()
    }
}

As an aside, one of the virtues of uploading and downloading using files rather than Data is that the memory footprint is smaller, avoiding the loading of the whole asset into memory at any given time. So, in the spirit of that, I use a small buffer for writing the contents of the image to the temporary file. This probably is not critical when uploading images, but may become essential when uploading larger assets, such as videos.

Regardless, the above also determines the mimetype of the asset using this extension:

extension URL {
    /// Mime type for the URL
    ///
    /// Requires `import UniformTypeIdentifiers` for iOS 14 solution.
    /// Requires `import MobileCoreServices` for pre-iOS 14 solution

    var mimeType: String {
        if #available(iOS 14.0, *) {
            return UTType(filenameExtension: pathExtension)?.preferredMIMEType ?? "application/octet-stream"
        } else {
            guard
                let identifier = UTTypeCreatePreferredIdentifierForTag(kUTTagClassFilenameExtension, pathExtension as CFString, nil)?.takeRetainedValue(),
                let mimeType = UTTypeCopyPreferredTagWithClass(identifier, kUTTagClassMIMEType)?.takeRetainedValue() as String?
            else {
                return "application/octet-stream"
            }

            return mimeType
        }
    }
}
  • Related