1

Header:

let header = ["Content-Type" : "application/x-www-form-urlencoded", "Authorization" : "Basic " + self.basicAuth];

Body:

var body : [String : AnyObject] = [:];
let body = ["grant_type" : "client_credentials", "scope" : "MessageSender"];

The Request and Serialization:

private func makeHTTPPostRequest(path: String, header: [String : String], body: [String: AnyObject], onCompletion: @escaping ServiceResponse) {
        let request = NSMutableURLRequest(url: NSURL(string: path)! as URL)

        // Set the method to POST
        request.httpMethod = "POST"

        do {
            // Set the POST body for the request
            let jsonBody = try JSONSerialization.data(withJSONObject: body, options: .prettyPrinted)
            request.httpBody = jsonBody
            let session = URLSession.shared
            request.allHTTPHeaderFields = header;

            let task = session.dataTask(with: request as URLRequest, completionHandler: {data, response, error -> Void in
                if let httpResponse = response as? HTTPURLResponse {
                    if let jsonData = data {
                        let json:JSON = JSON(data: jsonData)
                        print(response)
                        print(json)
                        onCompletion(json,httpResponse, error as NSError?)
                    } else {
                        onCompletion(JSON.null,HTTPURLResponse.init(), error as NSError?)
                    }
                }

            })
            task.resume()
        } catch {
            onCompletion(JSON.null,HTTPURLResponse.init(), nil)
        }
    }
}

When the request is done, it fires a 400 response with { "error_description" : "grant_type parameter is requiered field and it has to be non empty string.", "error" : "invalid_request" }

Obviously the body is not set correctly but I really don´t know why. I´m using this piece of code in other applications with no problem... . The same request works like charm in Postman. The body in postman is set with type x-www-form-urlencoded. Maybe the JSONSerialization is wrong ?

renpen
  • 23
  • 4
  • Are you calling function with empty body dictionary ? – Nirav D Mar 03 '17 at 12:06
  • No, the body is set. I debugged it to be sure. The dict is definitely set when JSONSerialization is called. – renpen Mar 03 '17 at 12:10
  • Check properly that it field is non empty or not – Nirav D Mar 03 '17 at 12:18
  • I tested it. I inserted a print(body) before request.httpMethod = "POST" is called. It prints: ["grant_type": client_credentials, "scope": MessageSender]. – renpen Mar 03 '17 at 12:25
  • Try this: `request.httpBody = "grant_type=client_credentials&scope=MessageSender".data(using: .utf8)`. Setting the result of `JSONSerialization` for the request with `Content-Type: application/x-www-form-urlencoded;` may not be accepted by usual servers. – OOPer Mar 03 '17 at 12:30
  • I was irritated that "client_credentials" is not a String. I casted the dict with var bodyT = body as! [String : String]; that both are strings but I wasn´t the solution. Of course the console now prints: ["grant_type": "client_credentials", "scope": "MessageSender"] but I still get 400 – renpen Mar 03 '17 at 12:30
  • @OOPer Thanks a lot. It works with the hardcoded String. Why isn´t it working with my dict? Have you a clue how I can fix it ? – renpen Mar 03 '17 at 13:07

1 Answers1

1

To send a POST request with Content-Type: application/x-www-form-urlencoded;, you need to create a URL query-like String and then convert it to a Data. Your code or any Swift Standard Library functions do not have the functionality. You may need to write it by yourself, or find a suitable third-party library. (Of course JSONSerialization is not suitable here, the String is not a JSON.)

With given a Dictionary<String, String>, you can do it like this:

var body: [String: String] = [:]
body = ["grant_type": "client_credentials", "scope": "MessageSender"]

(Simplified...)

request.httpBody = body.map{"\($0)=\($1)"}.joined(separator: "&").data(using: .utf8)
//`body.map{"\($0)=\($1)"}.joined(separator: "&")` -> grant_type=client_credentials&scope=MessageSender

(Strict... 4.10.22.6 URL-encoded form data)

extension CharacterSet {
    static let wwwFormUrlencodedAllowed = CharacterSet(charactersIn: "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._*" + "+")
}

extension String {
    var wwwFormUrlencoded: String {
        return self
            .replacingOccurrences(of: " ", with: "+")
            .addingPercentEncoding(withAllowedCharacters: .wwwFormUrlencodedAllowed)!
    }
}

class HTTPBody {
    static func wwwFormUrlencodedData(withDictionary dict: [String: String]) -> Data {
        return body
            .map{"\($0.wwwFormUrlencoded)=\($1.wwwFormUrlencoded)"}
            .joined(separator: "&").data(using: .utf8)!
    }
}

request.httpBody = HTTPBody.wwwFormUrlencodedData(withDictionary: body)

(Remember, not many servers interpret the received form data as strictly generated.)


One more, this is not a critical issue in this case, but you should better use Swift classes rather than NS-something:

typealias  ServiceResponse = (JSON, HTTPURLResponse?, Error?)->Void

private func makeHTTPPostRequest(path: String, header: [String : String], body: [String: String], onCompletion: @escaping ServiceResponse) {
    var request = URLRequest(url: URL(string: path)!)

    // Set the method to POST
    request.httpMethod = "POST"

    // Set the POST body for the request (assuming your server likes strict form data)
    request.httpBody = HTTPBody.wwwFormUrlencodedData(withDictionary: body)
    let session = URLSession.shared
    request.allHTTPHeaderFields = header;

    let task = session.dataTask(with: request, completionHandler: {data, response, error -> Void in
        if let httpResponse = response as? HTTPURLResponse {
            if let jsonData = data {
                let json:JSON = JSON(data: jsonData)
                print(response)
                print(json)
                onCompletion(json, httpResponse, error)
            } else {
                onCompletion(JSON.null, httpResponse, error)
            }
        }
    })
    task.resume()
}
OOPer
  • 47,149
  • 6
  • 107
  • 142
  • Also, setting the allHTTPHeaderFields value to a string is nonsensical; the code needs to add the Content-Type header correctly. And why is the original poster setting the body to a string and then populating it with a dictionary? And passing HTTP Basic authentication tokens via headers is absolutely not supported. Basic auth is understood by the framework, and your auth header will get stomped on. There are probably other mistakes. Those and the ones you mentioned are just the ones I noticed in a ten-second skim, as a non-Swift programmer. – dgatwood Mar 04 '17 at 18:23
  • @dgatwood, please do not take silence as affirmation here. If you want to say something more, you'd better post your own question. – OOPer Mar 05 '17 at 23:03
  • Why would I want to ask a question? I wrote most of the documentation for the API you're using. I'm pointing out things that are almost certainly errors in the original code. – dgatwood Mar 06 '17 at 05:09
  • Actually, I'm wrong about the first two bits. Apparently there is a setter for allHTTPHeaderFields, and I misread the Swift on the second point (I did say I'm not a Swift programmer). But my third point is definitely valid. Passing basic auth credentials via a header is not supported (and the docs explicitly say not to set that header). If it works at all, it's a fluke. – dgatwood Mar 06 '17 at 05:51
  • @dgatwood Thanks for your improvements. That´s the first time I´m working with URLRequest. In a first try I simply wanted to rebuild my Postman-Request in Swift. I´m very happy about your improvements ;). But I have a question,too. Why are there the possibility to set the header by my own with a String when it is not supported ? For my understanding, when it is not supported it should not be possible ? As I said, I´m happy about your improvements and will implement it in the app. – renpen Mar 06 '17 at 14:07
  • @renpen, maybe I need to note that not all servers implement Web standards strictly. For some servers, standard compliant code does not work, and some non-standard code actually works. So, my answer is written depending on your info -- _The same request works like charm in Postman_ or _It works with the hardcoded String_. Exactly the same code may not work for some other servers. In fact, writing a communication code with actual servers is not a simple thing. – OOPer Mar 06 '17 at 14:40
  • Sorry, I was wrong about the header setting thing. I'm just used to seeing it done with addValue:forHTTPHeaderField. Oh, wait, you mean for authentication... let me look for a reference.... – dgatwood Mar 06 '17 at 18:37
  • See this question: http://stackoverflow.com/questions/1973325/nsurlconnection-and-basic-http-authentication-in-ios and ignore the first answer. – dgatwood Mar 06 '17 at 18:41