I am trying to send a request that should contain both URL parameters and request body in JSON. Backend doesn't recognize the request and returns 400 status code.
This is how I tried to solve the problem
enum MyRequestsRouter: URLRequestConvertible {
case RequestA(paramA: String, bodyInJsonString: String),
RequestB(paramB: String)
var baseURL: URL {
URL(string: "http://127.0.0.1:8080")!
}
var method: HTTPMethod {
switch self {
case .RequestA: return .post
case .RequestB: return .get
}
}
var path: String {
switch self {
case .RequestA: return "/api/a"
case .RequestB: return "/api/b"
}
}
var parameters: Parameters? {
switch self {
case .RequestA(let paramA, let bodyInJsonString):
return [
"paramA": paramA
]
case .RequestB(let paramB):
return ["paramB": paramB]
default:
return nil
}
}
func asURLRequest() throws -> URLRequest {
let url = baseURL.appendingPathComponent(path)
var request = URLRequest(url: url)
request.method = method
switch self {
case let .RequestA(paramA, bodyInJsonString):
// here I am specifying `paramA` value
request = try URLEncoding.default.encode(request, with: parameters)
// here body is already serialized to Json
request.httpBody = Data(bodyInJsonString.utf8)
request.setValue("application/json", "Content-Type")
case let .RequestB(paramB):
request = try URLEncoding.default.encode(request, with: parameters)
}
return request
}
}
And this is how I am making API call
let json: Data = try JSONEncoder().encode(notSerializedBodyObject)
let jsonString = String(data: json, encoding: .utf8)
AF.request(MyRequestsRouter.RequestA(paramA: "paramA", bodyInJsonString: jsonString!)).response { response in
print("Request: \(response.request)")
print("Response: \(response.response)")
print("Error: \(response.error)")
// etc
}
}
As you can see the request URL is supposed to be
http://127.0.0.1:8080/api/a?paramA=paramA plus request body in JSON. But response is 400 Bad request.
How to properly configure Alomofire?
Alamofire's URLEncoding has an initializer that gets destination as parameter. Based on documentation:
Destination defining where the encoded query string will be applied.
.methodDependent by default.
By default .methodDependent case checks the HTTP method of the request and only on .get, .head and .delete requests it encodes the passed parameters in the URL.
Given that fact, you never see paramA in your request (not even in the body) because you override it in the following line:
// here body is already serialized to Json
request.httpBody = Data(bodyInJsonString.utf8)
So, instead of using the default URLEncoding, you can simply call URLEncoding's initializer directly passing as destination the .queryString case like this:
// here I am specifying `paramA` value
request = try URLEncoding(destination: .queryString).encode(request, with: parameters)
// here body is already serialized to Json
request.httpBody = Data(bodyInJsonString.utf8)
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
That way you will have a request with paramA in the URL and your JSON as a body.
Related
I am setting the basic auth token to my URLRequest and using Alamofire to execute.
I set 3 headers, content, accept and auth...content and accept are visible in the network traffic but auth is not. It is available if i print the headers of the URLRequest before its sent...
Any ideas here? as i dont see how its removing the auth header before posting
Code as follows:
// Construct url
let url = try APIConstants.baseUrl.asURL()
// Append path
var urlRequest = URLRequest(url: url.appendingPathComponent(path))
// Determine HTTP method
urlRequest.httpMethod = method.rawValue
let headers: HTTPHeaders = [
.contentType(APIConstants.ContentType.json.rawValue),
.accept(APIConstants.ContentType.json.rawValue),
]
if let token = token {
urlRequest.addValue("\(APIConstants.API.token.rawValue) \(token.key)",
forHTTPHeaderField: "Authorization")
}
urlRequest.headers = headers
// Add http body to request
if let parameters = parameters {
do {
let data = try JSONSerialization.data(withJSONObject: parameters, options: .prettyPrinted)
urlRequest.httpBody = data
} catch (_) {
print("APIRouter: Failed to parse body into request.")
}
}
//Encoding
let encoding: ParameterEncoding = {
switch method {
case .get:
return URLEncoding.default
default:
return JSONEncoding.default
}
}()
return try encoding.encode(urlRequest, with: parameters)
}
In my rest client i execute like this:
return Observable<T>.create { observer in
let request = AF.request(urlConvertible).responseDecodable { (response: DataResponse<T, AFError>) in
switch response.result {
case .success(let value):
observer.onNext(value)
observer.onCompleted()
case .failure(let error):
switch response.response?.statusCode {
case 403:
observer.onError(APIError.forbidden)
case 404:
observer.onError(APIError.notFound)
case 409:
observer.onError(APIError.conflict)
case 500:
observer.onError(APIError.internalServerError)
default:
observer.onError(error)
}
}
}
return Disposables.create {
request.cancel()
}
}
}
Edit:
Updated func to show further issue:
func asURLRequest() throws -> URLRequest {
// Construct url
let url = try APIConstants.baseUrl.asURL()
// Append path
var urlRequest = URLRequest(url: url.appendingPathComponent(path))
// Determine HTTP method
urlRequest.httpMethod = method.rawValue
let headers: HTTPHeaders = [
.contentType(APIConstants.ContentType.json.rawValue),
.accept(APIConstants.ContentType.json.rawValue),
.authorization("Token a5555485aa251b28fdsfasdfdsb379c131fddad")
]
urlRequest.headers = headers
// Add http body to request
if let parameters = parameters {
do {
let data = try JSONSerialization.data(withJSONObject: parameters, options: .prettyPrinted)
urlRequest.httpBody = data
} catch (_) {
print("APIRouter: Failed to parse body into request.")
}
}
//Encoding
let encoding: ParameterEncoding = {
switch method {
case .get:
return URLEncoding.default
default:
return JSONEncoding.default
}
}()
return try encoding.encode(urlRequest, with: parameters)
}
After setting the Authorization header in your URLRequest:
urlRequest.addValue("\(APIConstants.API.token.rawValue) \(token.key)", forHTTPHeaderField: "Authorization")
then you override all the request's headers by setting headers property:
urlRequest.headers = headers
A nice solution would be to update the headers that you already created above, like this:
var headers: HTTPHeaders = [
.contentType(APIConstants.ContentType.json.rawValue),
.accept(APIConstants.ContentType.json.rawValue),
]
if let token = token {
headers.add(.authorization("\(APIConstants.API.token.rawValue) \(token.key)"))
}
urlRequest.headers = headers
Another solution is to use KeyValue pair as follows:
var header: HTTPHeaders = [:]
if let token = getValueFromUserDefaults(keyName: "authToken") as? String {
header["Authorization"] = token
}
I always use this method. It's more handy for me.
I'm doing a POST call to server but Alamofire always send the body as a JSON and not as a Form URL Encoded, I do know that in oder to encode the body I have to insert data(using: .utf8, allowLossyConversion: false), but I don't know where.
How can I fix my code?
This is my actual code:
func asURLRequest() throws -> URLRequest {
let url = try DBank.StagingServer.baseUrl.asURL()
var urlRequest = URLRequest(url: url.appendingPathComponent(path))
// HTTP Method
urlRequest.httpMethod = method.rawValue
// Common Headers
headers.forEach { (field, value) in
urlRequest.setValue(value, forHTTPHeaderField: field)
}
// Parameters
if let parameters = parameters {
do {
urlRequest.httpBody = try JSONSerialization.data(withJSONObject: parameters, options: [])
} catch {
throw AFError.parameterEncodingFailed(reason: .jsonEncodingFailed(error: error))
}
}
I'm guessing you have response handler like below:
Alamofire.request(url, method: .post, parameters: params, encoding: URLEncoding(destination: .queryString), headers: headers)
.validate(statusCode: 200..<300)
.responseString { response in
//response.result.value will contain http response from your post call
}
With the result from this response you would set:
UserDefaults.standard.set("<result>", forKey: "<token>")
I try to get some data from server using URLSession.shared.dataTask.
It works fine, but I can't save result like a class variable.
Many answers recommend to use completion Handler, but it doesn't help for my task.
Here is my testing code:
class PostForData {
func forData(completion: #escaping (String) -> ()) {
if let url = URL(string: "http://odnakrov.info/MyWebService/api/test.php") {
var request = URLRequest(url: url)
request.httpMethod = "POST"
let postString : String = "json={\"Ivan Bolgov\":\"050-062-0769\"}"
print(postString)
request.httpBody = postString.data(using: .utf8)
let task = URLSession.shared.dataTask(with: request) {
data, response, error in
let json = String(data: data!, encoding: String.Encoding.utf8)!
completion(json)
}
task.resume()
}
}
}
class ViewController: UIViewController {
var str:String?
override func viewDidLoad() {
super.viewDidLoad()
let pfd = PostForData()
pfd.forData { jsonString in
print(jsonString)
DispatchQueue.main.async {
self.str = jsonString
}
}
print(str ?? "not init yet")
}
}
This closure is #escaping (i.e. it's asynchronously called later), so you have to put it inside the closure:
class ViewController: UIViewController {
#IBOutlet weak var label: UILabel!
var str: String?
override func viewDidLoad() {
super.viewDidLoad()
let pfd = PostForData()
pfd.performRequest { jsonString, error in
guard let jsonString = jsonString, error == nil else {
print(error ?? "Unknown error")
return
}
// use jsonString inside this closure ...
DispatchQueue.main.async {
self.str = jsonString
self.label.text = jsonString
}
}
// ... but not after it, because the above runs asynchronously (i.e. later)
}
}
Note, I changed your closure to return String? and Error? so that the the view controller can know whether an error occurred or not (and if it cares, it can see what sort of error happened).
Note, I renamed your forData to be performRequest. Generally you'd use even more meaningful names than that, but method names (in Swift 3 and later) should generally contain a verb that indicates what's being done.
class PostForData {
func performRequest(completion: #escaping (String?, Error?) -> Void) {
// don't try to build JSON manually; use `JSONSerialization` or `JSONEncoder` to build it
let dictionary = [
"name": "Ivan Bolgov",
"ss": "050-062-0769"
]
let jsonData = try! JSONEncoder().encode(dictionary)
// It's a bit weird to incorporate JSON in `x-www-form-urlencoded` request, but OK, I'll do that.
// But make sure to percent escape it.
let jsonString = String(data: jsonData, encoding: .utf8)!
.addingPercentEncoding(withAllowedCharacters: .urlQueryValueAllowed)!
let body = "json=" + jsonString
let url = URL(string: "http://odnakrov.info/MyWebService/api/test.php")!
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.httpBody = body.data(using: .utf8)
// It's not required, but it's good practice to set `Content-Type` (to specify what you're sending)
// and `Accept` (to specify what you're expecting) headers.
request.setValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type")
request.setValue("application/json", forHTTPHeaderField: "Accept")
// now perform the prepared request
let task = URLSession.shared.dataTask(with: request) { data, _, error in
guard let data = data, error == nil else {
completion(nil, error)
return
}
let responseString = String(data: data, encoding: .utf8)
completion(responseString, nil)
}
task.resume()
}
}
There are also some modifications to that routine, specifically:
Don't ever use ! forced unwrapping when processing server responses. You have no control over whether the request succeeds or fails, and the forced unwrapping operator will crash your app. You should gracefully unwrap these optionals with guard let or if let patterns.
It's exceedingly unusual to use json=... pattern where the ... is the JSON string. One can infer from that you're preparing a application/x-www-form-urlencoded request, and using $_POST or $_REQUEST to get the value associated with the json key. Usually you'd either do true JSON request, or you'd do application/x-www-form-urlencoded request, but not both. But to do both in one request is doubling the amount of work in both the client and server code. The above code follows the pattern in your original code snippet, but I'd suggest using one or the other, but not both.
Personally, I wouldn't have performRequest return the JSON string. I'd suggest that it actually perform the parsing of the JSON. But, again, I left this as it was in your code snippet.
I notice that you used JSON in the form of "Ivan Bolgov": "050-062-0769". I would recommend not using "values" as the key of a JSON. The keys should be constants that are defined in advantage. So, for example, above I used "name": "Ivan Bolgov" and "ss": "050-062-0769", where the server knows to look for keys called name and ss. Do whatever you want here, but your original JSON request seems to conflate keys (which are generally known in advance) and values (what values are associated with those keys).
If you're going to do x-www-form-urlencoded request, you must percent encode the value supplied, like I have above. Notably, characters like the space characters, are not allowed in these sorts of requests, so you have to percent encode them. Needless to say, if you did a proper JSON request, none of this silliness would be required.
But note that, when percent encoding, don't be tempted to use the default .urlQueryAllowed character set as it will allow certain characters to pass unescaped. So I define a .urlQueryValueAllowed, which removes certain characters from the .urlQueryAllowed character set (adapted from a pattern employed in Alamofire):
extension CharacterSet {
/// Returns the character set for characters allowed in the individual parameters within a query URL component.
///
/// The query component of a URL is the component immediately following a question mark (?).
/// For example, in the URL `http://www.example.com/index.php?key1=value1#jumpLink`, the query
/// component is `key1=value1`. The individual parameters of that query would be the key `key1`
/// and its associated value `value1`.
///
/// According to RFC 3986, the set of unreserved characters includes
///
/// `ALPHA / DIGIT / "-" / "." / "_" / "~"`
///
/// In section 3.4 of the RFC, it further recommends adding `/` and `?` to the list of unescaped characters
/// for the sake of compatibility with some erroneous implementations, so this routine also allows those
/// to pass unescaped.
static var 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
}()
}
I would suggest changing your PHP to accept a JSON request, e.g.:
<?php
// read the raw post data
$handle = fopen("php://input", "rb");
$raw_post_data = '';
while (!feof($handle)) {
$raw_post_data .= fread($handle, 8192);
}
fclose($handle);
// decode the JSON into an associative array
$request = json_decode($raw_post_data, true);
// you can now access the associative array how ever you want
if ($request['foo'] == 'bar') {
$response['success'] = true;
$response['value'] = 'baz';
} else {
$response['success'] = false;
}
// I don't know what else you might want to do with `$request`, so I'll just throw
// the whole request as a value in my response with the key of `request`:
$raw_response = json_encode($response);
// specify headers
header("Content-Type: application/json");
header("Content-Length: " . strlen($raw_response));
// output response
echo $raw_response;
?>
Then you can simplify the building of the request, eliminating the need for all of that percent-encoding that we have to do with x-www-form-urlencoded requests:
class PostForData {
func performRequest(completion: #escaping (String?, Error?) -> Void) {
// Build the json body
let dictionary = [
"name": "Ivan Bolgov",
"ss": "050-062-0769"
]
let data = try! JSONEncoder().encode(dictionary)
// build the request
let url = URL(string: "http://odnakrov.info/MyWebService/api/test.php")!
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.httpBody = data
// It's not required, but it's good practice to set `Content-Type` (to specify what you're sending)
// and `Accept` (to specify what you're expecting) headers.
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
request.setValue("application/json", forHTTPHeaderField: "Accept")
// now perform the prepared request
let task = URLSession.shared.dataTask(with: request) { data, _, error in
guard let data = data, error == nil else {
completion(nil, error)
return
}
let responseString = String(data: data, encoding: .utf8)
completion(responseString, nil)
}
task.resume()
}
}
I Keep getting an Error Code 400: Missing parameters
I believe the problem is the format i am sending the data in
API DOC: http://middleware.idxbroker.com/docs/api/methods/index.html#api-Leads-putLead
I believe the required data the call needs is the First Name, Last Name, and email, do I need to send the other field as nil or can i leave them blank?
API Call
/// Send Lead to IDX Broker
///
/// - Parameter lead: new Lead data fields
/// - Returns: configs for URL Session
class func putLead(lead: String) -> URLRequest {
let urlString = "https://api.idxbroker.com/leads/lead"
let url = NSURL(string: urlString)
var downloadTask = URLRequest(url: (url as URL?)!, cachePolicy: URLRequest.CachePolicy.reloadIgnoringCacheData, timeoutInterval: 20)
/******************** Add Headers required for API CALL *************************************/
downloadTask.setValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type")
downloadTask.setValue(APICalls.getAccessKey(), forHTTPHeaderField: "accesskey")
downloadTask.setValue("json", forHTTPHeaderField: "outputtype")
downloadTask.httpMethod = "PUT"
downloadTask.httpBody = lead.data(using: .utf8)
/******************** End Headers required for API CALL *************************************/
return downloadTask
}
Data Sent to Function
/// Create lead from text fields
let postString = "lead(firstName=\(String(describing: firstName.text!))&lastName=\(String(describing: lastName.text!))&=email=\(String(describing: Email.text!)))"
/// URL Encoding for put API
let escapedString = postString.addingPercentEncoding(withAllowedCharacters: .urlHostAllowed)
print(escapedString!)
print(escapedString?.data(using: .utf8) ?? "Error")
/// API Call with passing json String
let downloadTask = APICalls.putLead(lead: escapedString!)
URLSession.shared.dataTask(with: downloadTask, completionHandler: {(data, response, error) -> Void in
/// Status Returned from API CALL
if let httpResponse = response as? HTTPURLResponse {
print("statusCode: \(httpResponse.statusCode)")
}
}).resume()
/******** End URL Session **********/
400 means the API is not getting on or more of the parameters you are passing. If the API received firstName then it would respond with an error stating that lastName was not received. If fist and last are passed then it would respond with email not received.
Testing your code I was able to get the expected string when I didn't use lead() in postString.
I'm new to swift and iOS and trying to use Alamofire and router for them, which returns NSMutableURLRequest, but my code didn't work. So I just made one NSURLRequest for test, and requested it but results was same.
Here is my code. I'm currently using Alamofire and SwiftyJSON.
let params = ["Id": "1234567", "Token": "something"]
let url = NSURL(string: "myurl")
var request = NSMutableURLRequest(URL: url!)
request.HTTPMethod = Alamofire.Method.POST.rawValue
let encoding = Alamofire.ParameterEncoding.JSON
(request, _) = encoding.encode(request, parameters: params)
Alamofire.request(request)
.validate()
.responseJSON { response in
switch response.result {
case .Success:
if let value = response.result.value {
let json = JSON(value)
let token = json["token"].stringValue
let error = json["error"].stringValue
print("token : \(token), error : \(error)")
}
case .Failure(let error):
// TODO:
print(error)
}
}
Above code sends request without parameters. Are there any errors on my code?
I've checked your code and before executing encode function your request.HTTPBody is empty, but after it has some data like
Optional<NSData>
- Some:<7b22546f 6b656e22 3a22736f 6d657468 696e6722 2c224964 223a2231 32333435 3637227d>
When I call print(response.request?.HTTPBody) in Alamofire response block, I get the parameters as NSData and the HTTPBody includes the same data as before sending the request so it works.
Try also change the response from responseJSON to responseString, because if your response can't be parsed to JSON you get Failure.
I think you should check on your URL site if you get correct data.
Instead of your solution I use
Alamofire.request(method, url, parameters: parameters, encoding: .JSON) .responseString{ response in}
is the same but shorter and everything is as parameters.