URLComponents encoding
September 15, 2020Let's say we're working on a phone app and we need to query an API to get data about a phone number. As it is working worldwide we need to use international format +<countryCode><countryNumber>
. Thanks to URLComponents
our query can built as below:
var builder = URLComponents(string: "https://smsservice.com")!
builder.path = "/status"
builder.queryItems = [URLQueryItem(name: "number", value: "+33601020304")]
let requestURL = builder.url
Pretty simple and easy to read 💪. We now send our request to our server and get... a 400 http response 😱!
The devil is in the detail
The reason is pretty simple actually and lies into URLComponents
documentation. It explicitly state that queryItems
(the ones storing our phone number) are encoding along RFC 3986 🧨. Never heard about it? Put it simply it is a standard where plus sign is a valid query character meaning it does not need to be encoded.
Servers on the other hands usually follow URIs, URLs, and URNs: Clarifications and Recommendations 💣. And guess what? In this recommendation plus sign is a synonym for spacing in query meaning ?greeting=hello world
and ?greeting=hello+world
are the same thing.
As a result our server didn't see our number as "+33601020304" but actually as " 33601020304" 💥. To have our plus sign recognised by our server as such we have to manually encode it.
Same player play again
First we can do is defining a custom encoding function on URLQueryItem
and use it when adding ou query parameters:
extension URLQueryItem {
func percentEncoded() -> URLQueryItem {
/// addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) encode parameters following RFC 3986
/// which we need to encode other special characters correctly.
/// We then also encode "+" sign with its HTTP equivalent
var newQueryItem = self
newQueryItem.value = value?
.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed)?
.replacingOccurrences(of: "+", with: "%2B")
return newQueryItem
}
}
extension Array where Element == URLQueryItem {
func percentEncoded() -> Array<Element> {
return map { $0.percentEncoded() }
}
}
builder.queryItems = [URLQueryItem(name: "number", value: "+33601020304")].percentEncoded()
print(builder.url)
But that's not enough as queryItems
automatically encode each item which is what we usually want. Leading to double encoding special characters. To "disable" default behaviour we actually have to use another attribute:
builder.percentEncodedQueryItems = encodedQueryItems
print(builder.url)
And voila! You now are able to send a valid international number to your server.