Swift - POST request, sending JSON with Vapor 3 - json

I have a problem sending a POST request with Vapor 3 with the body including JSON. I am using https://docs.postman-echo.com/ to test this, where it responds back with the same JSON it is sent.
I have viewed answers on here, but got errors with encoding and content types.
router.get("hooray") { req -> Future<View> in
var postHeaders: HTTPHeaders = .init()
postHeaders.add(name: .contentType, value: "application/json")
postHeaders.add(name: .accept, value: "*/*")
postHeaders.add(name: .acceptEncoding, value: "gzip, deflate")
let oneField = singleGet(foo: "barfoobar")
// { foo: "barfoobar" } - JSON string
let encoder = JSONEncoder()
encoder.outputFormatting = .prettyPrinted
let jsonData = try encoder.encode(oneField)
let jsonString = String(data: jsonData, encoding: .utf8)!
let postBody = HTTPBody(string: jsonString)
let httpReq = HTTPRequest(method: .POST, url: "/post")
let httpRes = HTTPClient.connect(hostname: "postman-echo.com", on: req)
.flatMap(to: singleGet.self) { client in
req.http.headers = postHeaders
req.http.contentType = .json
req.http.body = postBody
return client.send(httpReq).flatMap(to: singleGet.self) { res in
return try req.content.decode(singleGet.self)
}
}
return try req.view().render("test", httpRes)
}
struct singleGet: Content {
let foo: String
}
I am getting the correct response with this code, but I was wondering when I rework the code to match this answer, why do I get errors?
I've added the headers and body to the request and comment them out inside the closure, but it won't work this way.
let httpReq = HTTPRequest(method: .POST, url: "/post", headers: postHeaders, body: postBody)

Related

Alamofire POST not working as expected. Error on parameters

I am trying to upload (post) a JSON string using Alamofire and I'm getting an error that leads me to believe there is some issue in the dictionary encoding.
The array I am encoding is an array of objects, all of type String. It looks like this:
class SavedDeliveries: Codable { //not sure if this is the issue (I use this to store default values locally)
var fullName = ""
var address = ""
var city = ""
var zip = ""
var phone = ""
var orders = ""
var ordersListed = ""
var pickup = ""
}
The code including the Alamofire call looks like this:
func postData() {
let headers: HTTPHeaders = [
"Content-Type": "application/json",
"X-Master-Key": "xxx", //API Key
"X-Bin-Name": "deliverydata"]
let jsonEncoder = JSONEncoder()
let jsonData = try! jsonEncoder.encode(deliveryList)
let json = String(data: jsonData, encoding: String.Encoding.utf8)
print(json!) // printing the JSON and it is correct when validated.
AF.request("https://api.jsonbin.io/v3/b", method: .post, parameters: json, encoding: JSONEncoding.default, headers: headers).responseString { (response) in
switch response.result {
case .success:
print("was successful")
case let .failure(error):
print(error)
}
}
}
I expect it to upload the JSON file but I'm getting an error message that says this:
Cannot convert value of type 'String?' to expected argument type 'Parameters?' (aka 'Optional<Dictionary<String, Any>>')
Not sure if the AF call parameter is expecting a certain kind of dictionary key:value format. If this is not the right call format for uploading JSON, what do I need to change?
Thanks for any help. I'm not a full-time Swift developer but manage an app that is usually within my capabilities. This one has me stumped.
I guess I don't understand much about HTTP requests or Alamofire, but I was able to solve this issue with the following mods to my code (without Alamofire, which seems like overkill in this case).
func postData() {
// Prepare URL
let url = URL(string: "https://api.jsonbin.io/v3/b")
guard let requestUrl = url else { fatalError() }
// Prepare URL Request Object
var request = URLRequest(url: requestUrl)
request.httpMethod = "POST"
// Set HTTP Request Body
let header: HTTPHeaders = [
"Content-Type": "application/json",
"X-Master-Key": "xxx",
"X-Bin-Name": "deliverydata"
]
request.headers = header
let jsonEncoder = JSONEncoder()
let jsonData = try! jsonEncoder.encode(deliveryList)
let json = String(data: jsonData, encoding: String.Encoding.utf8)
request.httpBody = json!.data(using: String.Encoding.utf8)
// Perform HTTP Request
let task = URLSession.shared.dataTask(with: request) { (data, response, error) in
// Check for Error
if let error = error {
print("Error took place \(error)")
return
}
// Convert HTTP Response Data to a String
if let data = data, let dataString = String(data: data, encoding: .utf8) {
print("Response data string:\n \(dataString)")
}
}
task.resume()
}
To make such a request using Alamofire, I'd recommend:
let headers: HTTPHeaders = [
"Content-Type": "application/json",
"X-Master-Key": "xxx", //API Key
"X-Bin-Name": "deliverydata"]
AF.request("https://api.jsonbin.io/v3/b",
method: .post,
parameters: deliveryList,
headers: headers).responseString { response in
switch response.result {
case .success:
print("was successful")
case let .failure(error):
print(error)
}
}
Even better, create a Decodable response type and use responseDecodable to parse it.
I would also suggest not using empty strings as your default values, that can easily lead to sending bad or invalid data to the backend.

How to send body parameter in Alamofire in Swift?

I have an api which consumes BODY parameter, like this
{"answers":[{"qid":2588,"value":["Free Society"]},{"qid":150,"value":["Closing of fSociety"]}],"uniqid":"t4815694"}
So what I have done is, I have created Data Model named SubmitAnswerModel like this which contains another Data Model named QuestionAnswersToSubmit
import Foundation
import ObjectMapper
import RealmSwift
class SubmitAnswerModel: Object {
var answers = [QuestionAnswersToSubmit]()
#objc dynamic var uniqid: String?
override static func primaryKey() -> String {
return "uniqid"
}
}
class QuestionAnswersToSubmit: Object {
#objc dynamic var qid = 0
var value = [String]()
override static func primaryKey() -> String {
return "qid"
}
}
So this object extends RealmObject and now I need to set values to this object and send it as BODY parameter in the api. I am doing it like this
func submitAnswerToApi() {
guard let token = UserDefault().getLoginAccessTokenKey() else {
print("No login token. Please relogin.")
return
}
let answersToSubmit = SubmitAnswerModel() //main model
let realm = try! Realm()
let savedExamResponse = realm.object(ofType: SavedExamResponse.self, forPrimaryKey: id)
answersToSubmit.uniqid = savedExamResponse?.uniqueId
var answerListToSubmit = [QuestionAnswersToSubmit]()
for item in (savedExamResponse?.questionAnswerList)! {
let answerToSubmit = QuestionAnswersToSubmit()
answerToSubmit.qid = item.questionId
answerToSubmit.value = [item.selectedOption]
answerListToSubmit.append(answerToSubmit)
}
answersToSubmit.answers = answerListToSubmit
let urlString = UrlCollection.submitAnswerUrl + "uniqid=" + answersToSubmit.uniqid! + "&token=" + token
let param = answersToSubmit
let uniqidParam = answersToSubmit.uniqid
dump(param)
var request = URLRequest(url: URL(string: urlString)!)
request.httpMethod = "POST"
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
if (!JSONSerialization.isValidJSONObject(answersToSubmit)) {
print("is not a valid json object")
//return
}
request.httpBody = try? JSONSerialization.data(withJSONObject: answersToSubmit)
Alamofire.request(request)
.responseJSON { response in
switch response.result {
case .failure(let error):
print(error)
if let data = response.data, let responseString = String(data: data, encoding: .utf8) {
print(responseString)
}
case .success(let responseObject):
print(responseObject)
}
}
}
So the problem now is, I am getting this error Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '*** +[NSJSONSerialization dataWithJSONObject:options:error:]: Invalid top-level type in JSON write'. What is the proper way to pass BODY parameter in SWIFT. Any help would be appreciated. Thanks!
Your Json format could be wrong, please refer to this question:
Invalid top-level type in JSON write'
Btw, the preferred way to set body using Alamofire is to pass a Dictionary to the parameters and then set encoding to URLEncoding.httpbody, like so:
let parameters: Parameters = ["foo": "bar"]
Alamofire.request("https://someapi/post", parameters: parameters, encoding: URLEncoding.httpbody)

Swift 4 - send application/x-www-form-urlencoded JSON

I want to encode my JSON to application/x-www-form-urlencoded
var req = URLRequest(url: url)
req.httpMethod = "POST"
req = JsonHelper.defineCommonHeaders(request: req)
var headers = req.allHTTPHeaderFields ?? [:]
headers["Content-Type"] = "application/x-www-form-urlencoded"
print(headers)
do {
let jsonData = try encoder.encode(self)
let jsonString = String(data: jsonData, encoding: .utf8)!
req.httpBody = jsonData as Data
print ("httpBody is: ", jsonString)
} catch {
//TODO:error handling
}
sending my JSON from Postman is fine, however sending from Swift 4 does not work.
I quess I need to encode empty key somewhere. There is great example POST request using application/x-www-form-urlencoded
but it does not cover how to do it in Swift 4. What I have to change?

Swift: send POST request with JSONEncoder encoded data does not work

I am using the JSONEncoder that has been provided with Swift4.
I have a class called Customer that uses the Codable protocol. Inside of Customer there are four Strings.
class Customer: Codable {
var title: String
var firstName: String
var lastName: String
var email: String
}
Reading JSON with a GET Request works fine.
URLSession.shared.dataTask(with: url) { (data, response, error) in
if error != nil {
print(error!.localizedDescription)
}
guard let data = data else { return }
do {
let customer = try JSONDecoder().decode(Customer.self, from: data)
DispatchQueue.main.async {
print("\(customer.title)")
print("\(customer.firstName)")
print("\(customer.lastName)")
print("\(customer.email)")
}
} catch let jsonError {
print(jsonError)
}
}.resume()
However when I start to do a POST request I am lost:
First set up the request object:
let urlString = "http://localhost:8000/customer"
guard let url = URL(string: urlString) else { return }
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.setValue("Application/json", forHTTPHeaderField: "Content-Type")
let customer = Customer()
customer.title = "Mr"
customer.firstName = "Chuck"
customer.lastName = "Norris"
customer.email = "chuck.norris#awsome.com"
let encodedData = try? JSONEncoder().encode(customer)
print(String(data: encodedData!, encoding: .utf8)!) //<- Looks as intended
// Output is {"firstName":"Chuck","lastName":"Norris","title":"MR","email":"chuck.norris#awesome.com "}
Now send it out
request.httpBody = encodedData //
URLSession.shared.dataTask(with: request, completionHandler: {(data, response, error) in
if let response = response {
print(response)
}
if let data = data {
do {
let json = try JSONSerialization.data(withJSONObject: data, options: [])
print(json)
}catch {
print(error)
}
}
})
In return I receive the message "The given data was not valid JSON."
So, my assumption is that I simply can not just put the encoded JSON data into the http body of my request.
Browsing through some articles about URLSession and JSON I found that it seems that I need to serialize my encoded JSON:
var json: Any?
if let data = encodedData {
json = try? JSONSerialization.jsonObject(with: data, options: .allowFragments)
}
Now, I don't know how to proceed. First I do not understand why I should serialize my JSON to something that can not simply put into a httpBody.
Because JSONSerialization.jsonObject produces Any? and not Data.
Update
Now, I was able to successfully send my data to my server. I am still trying to understand what was wrong - because I did not changed anything (except for the removal of the JSONSerialization call inside the completion Handler. I will investigate further...
...And .resume had been missing. :-)

Sending post data nsurlsession

Hi i'm trying to replicate the following curl command in swift using NSURLSession in a playground.
curl -k -i -H "Accept: application/json" -H "X-Application: " -X POST -d 'username=&password=' https://address.com/api/login
Here's what I've got so far. Where i'm struggling is that i'm unsure how to send the post data e.g.: 'username=&password='. Any help would be appreciated.
Thanks in advance.
import Foundation
import XCPlayground
// Let asynchronous code run
XCPSetExecutionShouldContinueIndefinitely()
let config = NSURLSessionConfiguration.defaultSessionConfiguration()
config.HTTPAdditionalHeaders = ["Accept" : "application/json", "X-Application" : "<AppKey>"]
let session = NSURLSession(configuration: config)
var running = false
let url = NSURL(string: "https://address.com/api/login")
let task = session.dataTaskWithURL(url!) {
(let data, let response, let error) in
if let httpResponse = response as? NSHTTPURLResponse {
let dataString = NSString(data: data, encoding: NSUTF8StringEncoding)
println(dataString)
}
running = false
}
running = true
task.resume()
You can create a mutable URLRequest and set the httpBody. But you should also percent escape the values for username and, more importantly, for password.
So, imagine your request being created like so:
let config = URLSessionConfiguration.default
config.httpAdditionalHeaders = ["Accept" : "application/json", "X-Application" : "<AppKey>", "Content-Type" : "application/x-www-form-urlencoded"]
let session = URLSession(configuration: config)
let url = URL(string: "https://identitysso.betfair.com/api/login")!
var request = URLRequest(url: url)
request.setBodyContent(["username": username, "password": password])
let task = session.dataTask(with: request) { data, response, error in
// make sure there wasn't a fundamental networking error
guard let data = data, let response = response as? HTTPURLResponse, error == nil else {
print(error ?? "Unknown error")
return
}
// if you're going to check for NSHTTPURLResponse, then do something useful
// with it, e.g. see if server status code indicates that everything is OK
guard 200 ..< 300 ~= response.statusCode else {
print("statusCode not 2xx; was \(response.statusCode)")
return
}
// since you set `Accept` to JSON, I'd assume you'd want to parse it;
// In Swift 4 and later, use JSONDecoder; in Swift 3 use JSONSerialization
do {
if let responseObject = try JSONSerialization.jsonObject(with: data) as? [String: AnyObject] {
print(responseObject)
}
} catch let parseError {
print(parseError)
print(String(data: data, encoding: .utf8) ?? data as NSData)
}
}
task.resume()
So the question is how setBodyContent builds the request body given a dictionary. Yes, you want to percent-escape anything not in the unreserved character set, but sadly CharacterSet.urlQueryAllowed is not up to the job. So you might do something like:
extension URLRequest {
/// Populate the HTTPBody of `application/x-www-form-urlencoded` request
///
/// - parameter parameters: A dictionary of keys and values to be added to the request
mutating func setBodyContent(_ parameters: [String : String]) {
let parameterArray = parameters.map { (key, value) -> String in
let encodedKey = key.addingPercentEncoding(withAllowedCharacters: .urlQueryValueAllowed)!
let encodedValue = value.addingPercentEncoding(withAllowedCharacters: .urlQueryValueAllowed)!
return "\(encodedKey)=\(encodedValue)"
}
httpBody = parameterArray
.joined(separator: "&")
.data(using: .utf8)
}
}
extension CharacterSet {
/// Character set containing characters allowed in query value as outlined in RFC 3986.
///
/// RFC 3986 states that the following characters are "reserved" characters.
///
/// - General Delimiters: ":", "#", "[", "]", "#", "?", "/"
/// - Sub-Delimiters: "!", "$", "&", "'", "(", ")", "*", "+", ",", ";", "="
///
/// In RFC 3986 - Section 3.4, it states that the "?" and "/" characters should not be escaped to allow
/// query strings to include a URL. Therefore, all "reserved" characters with the exception of "?" and "/"
/// should be percent-escaped in the query string.
///
/// - parameter string: The string to be percent-escaped.
///
/// - returns: The percent-escaped string.
static let urlQueryValueAllowed: CharacterSet = {
let generalDelimitersToEncode = ":#[]#" // does not include "?" or "/" due to RFC 3986 - Section 3.4
let subDelimitersToEncode = "!$&'()*+,;="
var allowed = CharacterSet.urlQueryAllowed
allowed.remove(charactersIn: generalDelimitersToEncode + subDelimitersToEncode)
return allowed
}()
}
Furthermore, I generally use a more complicated setBodyContent that also accepts numeric, boolean, and date types, but I didn't want to digress too far from your core question, how to properly build request for two string key/values pairs.
For Swift 2 rendition, see previous revision of this answer.
maybe this help, i use it to send post data:
var paramString = ""
var request:NSMutableURLRequest = NSMutableURLRequest(URL:url)
request.HTTPMethod = "POST"
var user = "MyUsername".stringByAddingPercentEscapesUsingEncoding(NSUTF8StringEncoding)
var pass = "MyPassword".stringByAddingPercentEscapesUsingEncoding(NSUTF8StringEncoding)
paramString = "username="+user!+"&password="+pass!
request.HTTPBody = paramString.dataUsingEncoding(NSUTF8StringEncoding)
let conf: NSURLSessionConfiguration = NSURLSession.sharedSession().configuration
let session = NSURLSession(configuration: conf)
session.dataTaskWithRequest(request) {
data, response, error in
let resp = NSString(data: data, encoding: NSUTF8StringEncoding)
}.resume()
NSUTF8StringEncoding can be replaced with whatever you need