Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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:).

like image 442
Artem Stepanenko Avatar asked Mar 27 '17 17:03

Artem Stepanenko


People also ask

What is URLComponents Swift?

Introducing URLComponents A few years ago, Apple added a type to the Foundation framework that makes working with URLs easier and more elegant, the URLComponents structure in Swift or, if you prefer Objective-C, the NSURLComponents class. Both types are available on iOS 7.0+, tvOS 9.0+, macOS 10.9+, and watchOS 2.0+.

How do I encode a percentage?

Percent-encoding is a mechanism to encode 8-bit characters that have specific meaning in the context of URLs. It is sometimes called URL encoding. The encoding consists of substitution: A '%' followed by the hexadecimal representation of the ASCII value of the replace character.

What is Urlqueryitem?

A single name-value pair from the query portion of a URL.

What is addingPercentEncoding?

addingPercentEncoding(withAllowedCharacters:)Returns a new string made from the receiver by replacing all characters not in the specified set with percent-encoded characters.

How to construct a URL in Swift?

Constructing URL is a routine task that every Swift developers do when building an iOS application. It’s very important to make sure the URL we construct are safe and correctly encoded using the percent encoding format. The most simple and crash prone approach to construct an URL is using the URL Struct String initalizer passing the raw string.

How to encode a string from a URL?

URL Encode A String Use the Stringmethod addingPercentEncoding(withAllowedCharacters:)to URL encode a string directly: var decoded = "url components" var encoded = decoded.addingPercentEncoding( withAllowedCharacters: .urlQueryAllowed ) // encoded is "url%20components" URL Property Examples URLs and Strings

How do I use urlcomponents and urlqueryitem?

URLComponents will automatically URL encode query items that contain disallowed characters: Use the String method addingPercentEncoding (withAllowedCharacters:) to URL encode a string directly: That’s it! By using URL, URLComponents, and URLQueryItem you can create custom urls, parse URL formats, and encode URL query items in Swift.

How do I URL encode query items that contain disallowed characters?

URLComponentswill automatically URL encode query items that contain disallowed characters: var components = URLComponents() components.queryItems = [ URLQueryItem(name: "page", value: "url components") ] // "?page=url%20components" components.string URL Encode A String


4 Answers

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
like image 87
Martin R Avatar answered Oct 23 '22 06:10

Martin R


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
like image 33
matt Avatar answered Oct 23 '22 06:10

matt


You can simply encode components.percentEncodedQuery after query items was inserted.

let characterSet = CharacterSet(charactersIn: "/+").inverted
components.percentEncodedQuery = components.percentEncodedQuery?.addingPercentEncoding(withAllowedCharacters: characterSet)
like image 44
Dmytro Dobrovolskyy Avatar answered Oct 23 '22 05:10

Dmytro Dobrovolskyy


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
like image 30
Forest Kunecke Avatar answered Oct 23 '22 04:10

Forest Kunecke