how to http post special chars in swift

You should be wary of using NSURLComponents because the NSURLQueryItem might percent-escape the character in question, the &, it doesn’t percent-escape the + character (which PHP will interpret as a space in conformance with the W3C Specification for x-www-form-urlencoded). As the queryItems documentation says:

Note

RFC 3986 specifies which characters must be percent-encoded in the query component of a URL, but not how those characters should be interpreted. The use of delimited key-value pairs is a common convention, but isn’t standardized by a specification. Therefore, you may encounter interoperability problems with other implementations that follow this convention.

One notable example of potential interoperability problems is how the plus sign (+) character is handled:

According to RFC 3986, the plus sign is a valid character within a query, and doesn’t need to be percent-encoded. However, according to the W3C recommendations for URI addressing, the plus sign is reserved as shorthand notation for a space within a query string (for example, ?greeting=hello+world).

This leaves a few alternatives for percent escaping the values you add to the query of the URL yourself if your value might include a + character:

  1. You can build your own CharacterSet of characters to be escaped and then use addingPercentEncodingForURLQueryValue in Swift 3:

    extension String {
    
        /// Returns a new string made from the `String` by replacing all characters not in the unreserved
        /// character set (as defined by RFC3986) with percent encoded characters.
    
        func addingPercentEncodingForURLQueryValue() -> String? {
            let allowedCharacters = CharacterSet.urlQueryValueAllowed()
            return addingPercentEncoding(withAllowedCharacters: allowedCharacters)
        }
    
    }
    
    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 / "-"https://stackoverflow.com/"."https://stackoverflow.com/"_"https://stackoverflow.com/"~"`
        ///
        /// 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 func urlQueryValueAllowed() -> CharacterSet {
            return CharacterSet(charactersIn: "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-._~/?")
        }
    
    }
    
  2. Alamofire takes a similar approach, but approaches this from the other direction, namely grabbing the .urlQueryAllowed character set (which is close, but not quite right), and takes out reserved characters identified in RFC 3986. In Swift 3:

    /// Returns a percent-escaped string following RFC 3986 for a query string key or value.
    ///
    /// RFC 3986 states that the following characters are "reserved" characters.
    ///
    /// - General Delimiters: ":", "#", "[", "]", "@", "?", "https://stackoverflow.com/"
    /// - Sub-Delimiters: "!", "$", "&", "'", "(", ")", "*", "+", ",", ";", "="
    ///
    /// In RFC 3986 - Section 3.4, it states that the "?" and "https://stackoverflow.com/" characters should not be escaped to allow
    /// query strings to include a URL. Therefore, all "reserved" characters with the exception of "?" and "https://stackoverflow.com/"
    /// should be percent-escaped in the query string.
    ///
    /// - parameter string: The string to be percent-escaped.
    ///
    /// - returns: The percent-escaped string.
    public func escape(_ string: String) -> String {
        let generalDelimitersToEncode = ":#[]@" // does not include "?" or "https://stackoverflow.com/" due to RFC 3986 - Section 3.4
        let subDelimitersToEncode = "!$&'()*+,;="
    
        var allowedCharacterSet = CharacterSet.urlQueryAllowed
        allowedCharacterSet.remove(charactersIn: "\(generalDelimitersToEncode)\(subDelimitersToEncode)")
    
        var escaped = ""
    
        //==========================================================================================================
        //
        //  Batching is required for escaping due to an internal bug in iOS 8.1 and 8.2. Encoding more than a few
        //  hundred Chinese characters causes various malloc error crashes. To avoid this issue until iOS 8 is no
        //  longer supported, batching MUST be used for encoding. This introduces roughly a 20% overhead. For more
        //  info, please refer to:
        //
        //      - https://github.com/Alamofire/Alamofire/issues/206
        //
        //==========================================================================================================
        if #available(iOS 8.3, *) {
            escaped = string.addingPercentEncoding(withAllowedCharacters: allowedCharacterSet) ?? string
        } else {
            let batchSize = 50
            var index = string.startIndex
    
            while index != string.endIndex {
                let startIndex = index
                let endIndex = string.index(index, offsetBy: batchSize, limitedBy: string.endIndex) ?? string.endIndex
                let range = startIndex..<endIndex
    
                let substring = string.substring(with: range)
    
                escaped += substring.addingPercentEncoding(withAllowedCharacters: allowedCharacterSet) ?? substring
    
                index = endIndex
            }
        }
    
        return escaped
    }
    

You could then use the above to percent escape the key and the value in the body of the request, for example:

let parameters = ["email" : email, "password" : password]
request.httpBody = parameters
    .map { (key, value) in
        let escapedKey = key.addingPercentEncodingForURLQueryValue()!
        let escapedValue = value.addingPercentEncodingForURLQueryValue()!
        return "\(escapedKey)=\(escapedValue)"
    }
    .joined(separator: "&")
    .data(using: .utf8)

For Swift 2 renditions of the above, see the previous revision of this answer.

Leave a Comment