I create an endpoint for my Moya API to post text and upload images at the same time to my remote RESTful service. Before trying Moya, I was using Alamofire natively for such POST request, however, I got http status code 413 (Entity is too large) with my Moya API abstraction layer. The same set of images (2) was used for both of my Alamofire native API and Moya API, and only Alamofire API works for me. So I am hoping to use some of the help here for troubleshooting:
The relevant code in my Moya API layer:
enum MyAPI {
case CreateObj([String: AnyObject])
}
extension MyAPI: TargetType {
var path: String {
switch self {
case .CreateObj(_):
return MyURI.CREATE_OBJ
}
}
var multipartBody: [MultipartFormData]? {
switch self {
case .CreateObj(let dictObj):
guard let imagesDataArr = dictObj["imagesDataArr"] as? [NSData] else { return[] }
let formData: [MultipartFormData] = imagesDataArr.map{MultipartFormData(provider: .Data($0), name: "images", mimeType: "image/jpeg", fileName: "photo.jpg")}
return formData
default:
return []
}
}
var method: Moya.Method {
switch self {
case .CreateObj:
return .POST
}
}
var parameters: [String: AnyObject]? {
switch self {
case .CreateObj(let objDict):
return objDict
default:
return nil
}
}
}
I am wondering if it could be due the parameters in the code above. Any ideas why I receive 413 status from my Moya API code? Thanks
I don't think that it's Moya witch causes this. Moya is a network abstraction layer that encapsulates calling Alamofire directly, if your request works with Alamofire, it should work work with Moya.
Did you try the same request with an Http client ? ( postman, Paw, ...)
@samirGuerdah
It may not be Moya that causes this error code 413, but my code of using Moya may cause this, because the multipart form data is a relatively new feature in Moya and there has not been much tutorial or documentation on how to use it.
I have tried Alamofire, PostMan in Chrome browser, and they all work until I came to try Moya
"name": "images" caught my eye, I expected that to have an extension, e.g. "name": "image.jpg". Also, you are returning the dictObj (aka objDict - you might want to check your naming on that one) from var parameters: [:]?, and so the imagesDataArr is going to be included there (maybe base64 encoded - I have no idea what Alamofire does with NSData in that case).
@colinta
My understanding is the "name": "images" is for specifying the field name in the form data to be "images", and this field is parsed by my remote REST api for uploading the images associated to this field. The field name fileName is for the image name with an extension: photo.jpg (see the code in my original post).
You said the imagesDataArr in my parameter dictObj would be included for the http request going out to the API, that made me think maybe I should have deleted the field imagesDataArr after creating the MultipartFormData object.
Thanks! - will report back if removing imagesDataArr works for me.
Oh shoot, you're totally right on the "name" thing! My mistake.
My code above works fine if I don't upload images when doing the POST request with the API, but it does not work with uploading images same time. I've confirmed my remote REST API works fine by using PostMan in Chrome browser.
The errors I see from uploading images and posting input texts same time are because of the other fields being undefined.
For example, my input text fields are title and content. These two input fields contain the correct string value when I do NOT upload images same time, however, these two fields turn out to be undefined when uploading images.
This made me think if I should also include my text inputs in the form data generated in the var multipartBody: [MultipartFormData]? block:
// current code
var multipartBody: [MultipartFormData]? {
switch self {
case .CreateObj(let dictObj):
guard let imagesDataArr = dictObj["imagesDataArr"] as? [NSData] else { return[] }
var formData: [MultipartFormData] = imagesDataArr.map{MultipartFormData(provider: .Data($0), name: "images", mimeType: "image/jpeg", fileName: "photo.jpg")}
return formData
default:
return []
}
}
If so, how should I convert my text input string to form data here? I've tried the following:
for (key, val) in questionDictObj {
formData.append(MultipartFormData(provider: .Data(String(val).dataUsingEncoding(NSUTF8StringEncoding, allowLossyConversion: false)!), name: key)) // does not work! the field's value is still undefined at the remote API
}
I don't see any examples for casting text input string values to MultipartFormData type. Anyone knows? How come the text input fields disappeared when they are POSTed together with images for uploading?
@rcholic Can you please compare the request details between an Http client and using Moya ( by using Charles proxy for example) ? I think that your code is the correct way to send a multipart data (Perhaps Moya or Alamofire are not setting correctly the content types).
your problem can be related to the issue #580
Can you provide a sample using Alamofire and Moya so we can know what's happening?
I saw that you return the same [NSData] array on parameters and you shouldn't do that. Here is an example of how to make a form with multipart-data:
enum SomeAPI {
case CreatePost(message:String, coordinate:CLLocationCoordinate2D,
contentType:Post.ContentType, contentFile:NSURL)
}
extension SomeAPI: TargetType {
var path:String {
switch self {
case .CreatePost: return "/posts"
}
}
var method: Moya.Method {
switch self {
case .CreatePost: return .POST
}
}
var parameters: [String: AnyObject]? {
switch self {
case .CreatePost(let message, let coordinate, let contentType, _):
return [
"message": message,
"lat": coodrintate.latitude,
"lon": coodrintate.longitude,
"type": contentType.rawValue
]
}
}
var multipartBody: [MultipartFormData]? {
switch self {
case .CreatePost(_, _, let contentType, let contentFile):
switch contentType {
case .Image:
return [MultipartFormData(provider: .File(contentFile), name: "files", mimeType:"image/jpeg", fileName: "photo.jpg")]
case .Video:
return [MultipartFormData(provider: .File(contentFile), name: "files", mimeType:"video/mp4", fileName: "video.mp4")]
}
}
}
}
This sample will post a form with lat, lon, message, type and files properties. I'm using a File multipart but you can use NSData directly.
All your text inputs should be on parameters property as string and data on multipartBody (Moya will take care of the rest).
BTW, I started the multipart upload implementation inside Moya. Today the code is much better than the first version, thanks to our great community :)
The lack of documentation is my fault too, I didn't have time to document this new feature and now I'm waiting for version 8.0 to document it properly (because it breaks our current API). For now, use this example to make your implementation and post here any question.
@leoneparise , below is my code in using Alamofire, my original post includes my Moya code:
Alamofire.upload(.POST, targetURL, headers: headers, multipartFormData: { (multipartFormData) -> Void in
for (key, val) in questionObj {
print("iterating key-val: \(key): \(val) in question JSON")
multipartFormData.appendBodyPart(data: String(val).dataUsingEncoding(NSUTF8StringEncoding, allowLossyConversion: false)!, name: key)
}
for i in 0..<self.selectedImageData.count {
let img = self.selectedImageData[i] // array of NSData from UIImage
multipartFormData.appendBodyPart(data: img, name: "images", fileName: "file\(i).png", mimeType: "image/png")
}
}, encodingCompletion: { (encodingResult) -> Void in
switch encodingResult {
case .Success(let upload, _, _):
upload.responseJSON(completionHandler: { (response) -> Void in
print("successful response: \(response)")
})
case .Failure(let error):
print("in encoding completion, error: \(error)")
}
})
So your example basically suggest that I should not include the images for uploading in the paramaters return, but rather should return images only in the multipartBody? Thanks
@rcholic Yes! only NSData should be on multipartBody everything else should be on parameters.
@rcholic Tell me if it's working now so I can close the Issue. :)
@leoneparise thanks so much with my wholeheart! Your help solved my problem completely! I'm closing it now :)
For folks running services behind Nginx, http://stackoverflow.com/questions/18908426/increasing-client-max-body-size-in-nginx-conf-on-aws-elastic-beanstalk
Most helpful comment
For folks running services behind Nginx, http://stackoverflow.com/questions/18908426/increasing-client-max-body-size-in-nginx-conf-on-aws-elastic-beanstalk