swift-openapi-generator
swift-openapi-generator copied to clipboard
How to send a file in multipart/form-data?
Question
I'm using your package to generate my APIs which has been working great for me. However, as soon as I started working with uploading files, I stumbled upon an issue with multipart/form-data
. Whenever I use the generated API, it sends the request with the file having a mimetype of application/octet-stream
. Which is something that I am not supporting on the backend nor do I have plans to do so. If I'm uploading a jpeg image, I expect the mimetype to be image/jpeg
. How do I get this desired behavior?
This is how I create data from a UIImage:
func imageToMultipartData(_ image: UIImage) async throws -> Data {
let boundary = UUID().uuidString
var data = Data()
data.append("\r\n--\(boundary)\r\n".data(using: .utf8)!)
data.append("Content-Disposition: form-data; name=\"file\"; filename=\"image.jpeg\"\r\n".data(using: .utf8)!)
data.append("Content-Type: image/jpeg\r\n\r\n".data(using: .utf8)!)
data.append(image.jpegData(compressionQuality: 1.0)!)
data.append("\r\n--\(boundary)--\r\n".data(using: .utf8)!)
return data
}
This is how I'm using the generated API:
guard let image = viewModel.selectedImage else {
throw URLError(.cannotDecodeRawData)
}
let data = try await imageToMultipartData(image)
let response = try await HttpClient.updateProfileImage(
body: .multipartForm([
.file(.init(
payload: .init(body: HTTPBody(data)),
filename: "file.jpeg"
))
])
)
This is my schema for the method:
/users/profile/image:
post:
operationId: updateProfileImage
parameters: []
requestBody:
required: true
content:
multipart/form-data:
schema:
$ref: '#/components/schemas/UploadImageDto'
responses:
'201':
description: ''
content:
application/json:
schema:
$ref: '#/components/schemas/UserProfileDto'
'400':
description: Bad request
content:
application/json:
schema:
$ref: '#/components/schemas/ValidationErrorDto'
Where UploadImageDto
component have this schema:
UploadImageDto:
type: object
properties:
file:
type: string
format: binary
required:
- file
Hey @yousifalraheem. We have an example package for client and server that show how to handle various request and response content types, including multipart here: https://github.com/apple/swift-openapi-generator/tree/main/Examples/various-content-types-client-example.
If that doesn't help, let us know and we can take a closer look at your use case specifically.
Hi @yousifalraheem,
it seems you're manually serializing the multipart part, but that's not correct - the generated code already does that. You need to simplify your Data creation to just:
func imageToMultipartData(_ image: UIImage) async throws -> Data {
image.jpegData(compressionQuality: 1.0)!
}
Hi @yousifalraheem - were you able to get it working?
@czechboy0 .. I failed with every attempt to use the generated function so I ended up with manually creating the entire HTTP request with URLSession which is unfortunate. My upload function and its utility looked something like this:
func imageToMultipartData(_ image: UIImage, boundary: String, fieldName: String = "file") async throws -> Data {
let ext = ".jpeg"
let mimeType = "image/jpeg"
var data = Data()
data.append("\r\n--\(boundary)\r\n".data(using: .utf8)!)
data.append("Content-Disposition: form-data; name=\"\(fieldName)\"; filename=\"image\(ext)\"\r\n".data(using: .utf8)!)
data.append("Content-Type: \(mimeType)\r\n\r\n".data(using: .utf8)!)
data.append(image.jpegData(compressionQuality: 1.0)!)
data.append("\r\n--\(boundary)--\r\n".data(using: .utf8)!)
return data
}
func uploadProfileImage() async {
do {
guard let image = viewModel.selectedImage else {
return
}
let boundary = UUID().uuidString
let url = URL(string: "https://mysite.com/users/profile/image")!
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.setValue("multipart/form-data; boundary=\(boundary)", forHTTPHeaderField: "Content-Type")
request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
let data = try await imageToMultipartData(image, boundary: boundary)
let (_, _) = try await URLSession.shared.upload(for: request, from: data)
viewModel.imageSelection = nil
} catch {
uploadError = error.localizedDescription
}
}
It's not ideal but at least it worked. I believe this complexity could've been avoided. Maybe an overload init function can be added that accepts just a UIImage instead of HTTPBody and it would deal with creating the multipart request correctly using the schema which clearly specifies the property name and type. When I built the web app for the same platform, the generated upload function accepts a value of type File and it would do the rest flawlessly. If I would suggest a feature, it would be to simplify this process if possible.
You create an HTTPBody from a UIImage like this:
let image: UIImage = ...
let body = HTTPBody(image.jpegData(compressionQuality: 1.0)!)
And then you create the file part with this data, and you can specify the content disposition fields, like filename as well.
If you could clarify which parts weren't working for you, we can try to help.
Closing, please reopen if you think there's a bug in Swift OpenAPI Generator.