Search code examples
iosswiftswift3nsurl

Encode '+' using URLComponents in Swift


This is how I add query params to a base URL:

let baseURL: URL = ...
let queryParams: [AnyHashable: Any] = ...
var components = URLComponents(url: baseURL, resolvingAgainstBaseURL: false)
components?.queryItems = queryParams.map { URLQueryItem(name: $0, value: "\($1)") }
let finalURL = components?.url

The problem emerges when one of the values contains a + symbol. For some reason it's not encoded to %2B in the final URL, instead, it stays +. If I do encoding myself and pass %2B, NSURL encodes % and the 'plus' becomes %252B.

The question is how can I have %2B in the instance of NSURL?

P.S. I know, I wouldn't even have this problem if I constructed a query string myself and then simply pass a result to the NSURL's constructor init?(string:).


Solution

  • As pointed out in the other answers, the "+" character is valid in a query string, this is also stated in the query​Items documentation:

    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).
    [...]
    Depending on the implementation receiving this URL, you may need to preemptively percent-encode the plus sign character.

    And the W3C recommendations for URI addressing state that

    Within the query string, the plus sign is reserved as shorthand notation for a space. Therefore, real plus signs must be encoded. This method was used to make query URIs easier to pass in systems which did not allow spaces.

    This can be achieved by "manually" building the percent encoded query string, using a custom character set:

    let queryParams = ["foo":"a+b", "bar": "a-b", "baz": "a b"]
    var components = URLComponents()
    
    var cs = CharacterSet.urlQueryAllowed
    cs.remove("+")
    
    components.scheme = "http"
    components.host = "www.example.com"
    components.path = "/somepath"
    components.percentEncodedQuery = queryParams.map {
        $0.addingPercentEncoding(withAllowedCharacters: cs)!
        + "=" + $1.addingPercentEncoding(withAllowedCharacters: cs)!
    }.joined(separator: "&")
    
    let finalURL = components.url
    // http://www.example.com/somepath?bar=a-b&baz=a%20b&foo=a%2Bb
    

    Another option is to "post-encode" the plus character in the generated percent-encoded query string:

    let queryParams = ["foo":"a+b", "bar": "a-b", "baz": "a b"]
    var components = URLComponents()
    components.scheme = "http"
    components.host = "www.example.com"
    components.path = "/somepath"
    components.queryItems = queryParams.map { URLQueryItem(name: $0, value: $1) }
    components.percentEncodedQuery = components.percentEncodedQuery?
        .replacingOccurrences(of: "+", with: "%2B")
    
    let finalURL = components.url
    print(finalURL!)
    // http://www.example.com/somepath?bar=a-b&baz=a%20b&foo=a%2Bb