Encode '+' using URLComponents in Swift

2019-02-21 23:23发布

This is how I add a 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 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:).

3条回答
Evening l夕情丶
2楼-- · 2019-02-21 23:45

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

On the other hand, 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
查看更多
干净又极端
3楼-- · 2019-02-21 23:50

Can you try using addingPercentEncoding(withAllowedCharacters: .alphanumerics)?

I just put together a quick playground demonstrating how this works:

//: Playground - noun: a place where people can play

let baseURL: URL = URL(string: "http://example.com")!
let queryParams: [AnyHashable: Any] = ["test": 20, "test2": "+thirty"]
var components = URLComponents(url: baseURL, resolvingAgainstBaseURL: false)

var escapedComponents = [String: String]()
for item in queryParams {
    let key = item.key as! String
    let paramString = "\(item.value)"

    // percent-encode any non-alphanumeric character.  This is NOT something you typically need to do.  User discretion advised.
    let escaped = paramString.addingPercentEncoding(withAllowedCharacters: .alphanumerics)

    print("escaped: \(escaped)")

    // add the newly escaped components to our dictionary
    escapedComponents[key] = escaped
}


components?.queryItems = escapedComponents.map { URLQueryItem(name: ($0), value: "\($1)") }
let finalURL = components?.url
查看更多
ら.Afraid
4楼-- · 2019-02-21 23:54

URLComponents is behaving correctly: the + is not being percent-encoded because it is legal as it stands. You can force the + to be percent-encoded by using .alphanumerics, as explained already by Forest Kunecke (I got the same result independently but he was well ahead of me in submitting his answer!).

Just a couple of refinements. The OP's value: "\($1)" is unnecessary if this is a string; you can just say value:$1. And, it would be better to form the URL from all its components.

This, therefore, is essentially the same solution as Forest Kunecke, but I think it is more canonical and it is certainly more compact ultimately:

let queryParams = ["hey":"ho+ha"]
var components = URLComponents()
components.scheme = "http"
components.host = "www.example.com"
components.path = "/somepath"
components.queryItems = queryParams.map { 
  URLQueryItem(name: $0, 
    value: $1.addingPercentEncoding(withAllowedCharacters: .alphanumerics)!) 
}
let finalURL = components.url

EDIT Rather better, perhaps, after suggested correction from Martin R: we form the entire query and percent-encode the pieces ourselves, and tell the URLComponents that we have done so:

let queryParams = ["hey":"ho+ha", "yo":"de,ho"]
var components = URLComponents()
components.scheme = "http"
components.host = "www.example.com"
components.path = "/somepath"
var cs = CharacterSet.urlQueryAllowed
cs.remove("+")
components.percentEncodedQuery = queryParams.map {
    $0.addingPercentEncoding(withAllowedCharacters: cs)! + 
    "=" + 
    $1.addingPercentEncoding(withAllowedCharacters: cs)!
}.joined(separator:"&")

// ---- Okay, let's see what we've got ----
components.queryItems
// [{name "hey", {some "ho+ha"}}, {name "yo", {some "de,ho"}}]
components.url
// http://www.example.com/somepath?hey=ho%2Bha&yo=de,ho
查看更多
登录 后发表回答