When I upload a file using AFNetworking v3.1.0 multipartFormRequestWithMethod
and include a parameter dictionary, which is converted to JSON, and the dictionary has an entry such as:
["Test": 1]
that is, a key with a numeric value, the JSON structure when it arrives at my server is a quoted string. For example:
{"Test":"1"}
This is not what I want. What I want received on the server is:
{"Test": 1}
Note that I do not get this problem when using the POST method of AFHTTPSessionManager (which I believe doesn't allow for file upload).
Is this a bug in AFNetworking? Is it something I'm doing? Is there a workaround or other solution? I've seen the discussion at AFNetworking JSON serialization problems but my application isn't really suited to parsing on the server to get this back to the way I'd like it (numbers without the quotes). Thoughts?
Here's my client code which uses AFNetworking:
import Foundation
import SMCoreLib // some common library functions I use
class Server {
private let manager: AFHTTPSessionManager!
internal static let session = Server()
var uploadTask:NSURLSessionUploadTask?
private init() {
self.manager = AFHTTPSessionManager()
// https://stackoverflow.com/questions/26604911/afnetworking-2-0-parameter-encoding
self.manager.responseSerializer = AFJSONResponseSerializer()
// This does appear necessary for requests going out to server to receive properly encoded JSON parameters on the server.
self.manager.requestSerializer = AFJSONRequestSerializer()
self.manager.requestSerializer.setValue("application/json", forHTTPHeaderField: "Content-Type")
}
// In the completion hanlder, if error != nil, there will be a non-nil serverResponse.
internal func sendServerRequestTo(toURL serverURL: NSURL, withParameters parameters:[String:AnyObject],
completion:((serverResponse:[String:AnyObject]?, error:NSError?)->())?) {
Log.special("serverURL: \(serverURL)")
if !Network.connected() {
Log.msg("Network not connected!")
completion?(serverResponse: nil, error: Error.Create("Network not connected!"))
return
}
self.manager.POST(serverURL.absoluteString, parameters: parameters, progress: nil,
success: { (request:NSURLSessionDataTask, response:AnyObject?) in
if let responseDict = response as? [String:AnyObject] {
Log.msg("AFNetworking Success: \(response)")
completion?(serverResponse: responseDict, error: nil)
}
else {
completion?(serverResponse: nil, error: Error.Create("No dictionary given in response"))
}
},
failure: { (request:NSURLSessionDataTask?, error:NSError) in
print("**** AFNetworking FAILURE: \(error)")
completion?(serverResponse: nil, error: error)
})
}
// withParameters must have a non-nil key SMServerConstants.fileMIMEtypeKey
internal func uploadFileTo(serverURL: NSURL, withParameters parameters:[String:AnyObject]?, completion:((serverResponse:[String:AnyObject]?, error:NSError?)->())?) {
Log.special("serverURL: \(serverURL)")
if !Network.connected() {
completion?(serverResponse: nil, error: Error.Create("Network not connected."))
return
}
var error:NSError? = nil
let request = AFJSONRequestSerializer().multipartFormRequestWithMethod("POST", URLString: serverURL.absoluteString, parameters: parameters, constructingBodyWithBlock: nil, error: &error)
if nil != error {
completion?(serverResponse: nil, error: error)
return
}
self.uploadTask = self.manager.uploadTaskWithStreamedRequest(request, progress: { (progress:NSProgress) in
},
completionHandler: { (request: NSURLResponse, responseObject: AnyObject?, error: NSError?) in
if (error == nil) {
if let responseDict = responseObject as? [String:AnyObject] {
Log.msg("AFNetworking Success: \(responseObject)")
completion?(serverResponse: responseDict, error: nil)
}
else {
let error = Error.Create("No dictionary given in response")
Log.error("**** AFNetworking FAILURE: \(error)")
completion?(serverResponse: nil, error: error)
}
}
else {
Log.error("**** AFNetworking FAILURE: \(error)")
completion?(serverResponse: nil, error: error)
}
})
if nil == self.uploadTask {
completion?(serverResponse: nil, error: Error.Create("Could not start upload task"))
return
}
self.uploadTask?.resume()
}
}
I've used a nil parameter value for constructingBodyWithBlock
because the issue arises when I upload a file and when I don't (this example doesn't actually upload a file).
Here's the code that calls those methods:
import UIKit
import SMCoreLib
class ViewController: UIViewController {
//let serverURL = NSURL(string: "http://192.168.3.228:8082/json/")!
let serverURL = NSURL(string: "http://192.168.3.228:8082/upload/")!
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
let params = ["Test" : 1]
Server.session.uploadFileTo(serverURL, withParameters:params){ (serverResponse, error) in
Log.msg("serverResponse: \(serverResponse); error: \(error)")
}
/*
Server.session.sendServerRequestTo(toURL: self.serverURL, withParameters: params) { (serverResponse, error) in
Log.msg("serverResponse: \(serverResponse); error: \(error)")
}*/
}
}
For completeness, I'll include the Node.js server I'm using for testing:
'use strict';
var express = require('express');
var bodyParser = require('body-parser');
var app = express();
var multer = require('multer');
// https://stackoverflow.com/questions/4295782/how-do-you-extract-post-data-in-node-js
app.use(bodyParser.json({extended : true}));
const fileUploadFieldName = "uploadFile";
const initialUploadDirectory = "./uploadedFiles";
// See https://stackoverflow.com/questions/31496100/cannot-app-usemulter-requires-middleware-function-error
// See also https://codeforgeek.com/2014/11/file-uploads-using-node-js/
// TODO: Limit the size of the uploaded file.
// TODO: Is there a way with multer to add a callback that gets called periodically as an upload is occurring? We could use this to "refresh" an activity state for a lock to make sure that, even with a long-running upload (or download) if it is still making progress, that we wouldn't lose a lock.
var upload = multer({ dest: initialUploadDirectory}).single(fileUploadFieldName);
function handleBody(request, response) {
response.setHeader('Content-Type', 'application/json');
console.log("Got request!");
const json = request.body["Test"];
console.log("json: " + json);
const fullBody = JSON.stringify(request.body);
console.log("fullBody: " + fullBody);
response.end(fullBody);
}
app.post("/json" , function(request, response) {
handleBody(request, response);
});
app.post("/upload", upload, function (request, response) {
handleBody(request, response);
});
app.set('port', 8082);
app.listen(app.get('port'), function() {
console.log('Node app is running on port', app.get('port'));
});
And here's the package.json for the server:
{
"name": "jsonserver",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "ISC",
"dependencies": {
"body-parser": "^1.15.1",
"express": "^4.13.4",
"multer": "^1.1.0"
}
}