Search code examples
swiftpostswift3nsurlrequest

set body in NSMutableURLRequest doesn´t work


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 ?


Solution

  • 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()
    }