Issue
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()
Solution
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
}
}
}
Answered By - Rob
Answer Checked By - Willingham (JavaFixing Volunteer)