When using URLComponents
's queryItems
I've found that if you have a query item whose value contains some percent encoded characters, in my case a /
being encoded as %2F
, then if you construct a URLComponents
object from a String
URL that contains such a query item, then mutate the list of query items for the URLComponents
object, then if you try to get a URL
by calling .url
on the URLComponents
object, then the query items lose their percent encoding.
Here's the code I've been testing this with in a playground:
import UIKit
// --- Part 1 ---
print("--- Part 1 ---\n")
let startURL = "https://test.com/test.jpg?X-Test-Token=FQdzEPH%2F%2F%2F"
var components = URLComponents(string: startURL)!
if let compURL = components.url {
print(URL(string: startURL)! == compURL) // True
print(startURL)
print(compURL)
}
// --- Part 2 ---
print("\n--- Part 2 ---\n")
let startURLTwo = "https://test.com/test.jpg?X-Test-Token=FQdzEPH%2F%2F%2F"
let finalURL = "https://test.com/test.jpg?X-Test-Token=FQdzEPH%2F%2F%2F&foo=bar"
var componentsTwo = URLComponents(string: startURLTwo)!
let extraQueryItem = URLQueryItem(name: "foo", value: "bar")
componentsTwo.queryItems!.append(extraQueryItem)
if let compURLTwo = componentsTwo.url {
print(URL(string: finalURL)! == compURLTwo) // False
print(finalURL)
print(compURLTwo)
}
Here's an image if that makes it easier to understand what's going on:
You should use percentEncodedQuery
if you have a query that is already percent encoded:
let startURL = "https://test.com/test.jpg"
var components = URLComponents(string: startURL)!
components.percentEncodedQuery = "X-Test-Token=FQdzEPH%2F%2F%2F"
if let compURL = components.url {
print(compURL)
}
Or you can specify it unescaped (and it leaves it unescaped as it's not necessary to escape /
characters in a query):
let startURL = "https://test.com/test.jpg"
var components = URLComponents(string: startURL)!
components.queryItems = [URLQueryItem(name: "X-Test-Token", value: "FQdzEPH///")]
if let compURL = components.url {
print(compURL)
}
And if you have to update queryItems
, just make sure to set percentEncodedQuery
at the very end:
let startURL = "https://test.com/test.jpg"
let encodedQuery = "X-Test-Token=FQdzEPH%2F%2F%2F"
var components = URLComponents(string: startURL)!
components.queryItems = [URLQueryItem(name: "foo", value: "bar, baz, & qux")]
if let query = components.percentEncodedQuery {
components.percentEncodedQuery = query + "&" + encodedQuery
} else {
components.percentEncodedQuery = encodedQuery
}
if let compURL = components.url {
print(compURL)
}
RFC 3986 specifically states that a URL query may contain the /
character. It doesn't need to be percent encoded. URLComponents
is simply following the standard and unencoding the %2F
to /
when you specifically modify any of the query parameters.
In the first case you don't modify anything at all so the URL stays unchanged. In the 2nd, you modify the query parameters property of the components. So the URLComponents
builds a new query string from that updated array of query parameters. In the process, if normalizes all of them and the unnecessary percent encoding is removed.