1

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()
Goga
  • 349
  • 1
  • 16

1 Answers1

1

In those other examples you found (e.g., Upload image with parameters in Swift), we 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(URL)
    }

    /// 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 let pointer = buffer.baseAddress?.assumingMemoryBound(to: UInt8.self) else {
                throw OutputStreamError.bufferFailure
            }

            try write(buffer: pointer, length: buffer.count)
        }
    }

    /// 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(fileURL)
        }

        inputStream.open()
        defer { inputStream.close() }

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

        while inputStream.hasBytesAvailable {
            let length = inputStream.read(buffer, maxLength: bufferSize)
            if length < 0 {
                throw OutputStreamError.readFailure(fileURL)
            } else if length > 0 {
                try write(buffer: buffer, length: length)
            }
        }
    }
}

private extension OutputStream {
    /// Writer buffer to output stream.
    ///
    /// This will loop until all bytes are written. On failure, this throws an error
    ///
    /// - Parameters:
    ///   - buffer: Unsafe pointer to the buffer.
    ///   - length: Number of bytes to be written.

    func write(buffer: UnsafePointer<UInt8>, length: Int) throws {
        var bytesRemaining = length
        var pointer = buffer

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

            bytesRemaining -= bytesWritten
            pointer += bytesWritten
        }
    }
}

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
        }
    }
}
Rob
  • 415,655
  • 72
  • 787
  • 1,044