Reputation: 38727
in Swift 3.x with Xcode 9 beta 2, using addingPercentEncoding
gives unexpected results. CharacterSet.urlPathAllowed
always contains ":", so by definition of addingPercentEncoding
, it should never escape it. Yet, using this code:
// always true
print(CharacterSet.urlPathAllowed.contains(":"))
let myString = "info:hello world"
let escapedString = myString.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed)!
print(escapedString)
I get those results:
true
info%3Ahello%20world
true
info:hello%20world
Is there any workaround to get a working implementation of addingPercentEncoding
that will correctly respect the given allowedCharacters
?
Upvotes: 8
Views: 8789
Reputation: 21
Calling iOS .addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed)
replace only minimal set of special symbols .urlQueryAllowed
: ("#%<>[]^`{|} ).
So, this situation may lead to problems on the server side, because some special symbols (for example +) cannot be processed by some kind of backend software.
We need to extend the set of special encoded symbols adding +:@&,;=?/$
.
That characters I got by comparing encoded characters in Android Studio and iOS.
Use the function convert(param: value)
and components.percentEncodedQueryItems
instead components.queryItems
.
Calling components.queryItems
with custom function convert
will lead to double encoding of urlQueryAllowed and error.
func makeRequest() async throws {
let queryParams: [String: Any] = ["name": "Leanne Graham"]
guard let baseUrl = URL(string: "https://jsonplaceholder.typicode.com/users"),
var components = URLComponents(url: baseUrl, resolvingAgainstBaseURL: false) else {
return
}
// Use `.percentEncodedQueryItems` instead of `.queryItems`! To avoid double encoding!
components.percentEncodedQueryItems = queryParams.map { key, value in
URLQueryItem(name: key, value: convert(param: value))
}
guard let url = components.url else { return }
let result = try await URLSession.shared.data(for: URLRequest(url: url))
print(result)
}
func convert(param: Any?) -> String? {
if param == nil { return nil }
switch param {
case let boolValue as Bool: return "\(boolValue)"
case let numberValue as NSNumber: return "\(numberValue)"
case let stringValue as String:
return stringValue.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowedExtended) ?? stringValue
default:
assertionFailure("Add a handler for the parameter type: \(type(of: param))")
return ""
}
}
extension CharacterSet {
public static let urlQueryAllowedExtended: CharacterSet = {
// urlQueryAllowed provide encoding for symbols: "\"#%<>[\]^`{|} "
// We need extend it with following symbols: "+:@&,;=?/$"
CharacterSet.urlQueryAllowed.subtracting(CharacterSet(charactersIn: "+:@&,;=?/$"))
}()
}
Example code with encoding on github with explanation and unit-test
Upvotes: 0
Reputation: 438122
The reason that it is now percent escaping the :
character is that .urlPathAllowed
now strictly observes RFC 3986, which says in section 3.3, “Paths”:
In addition, a URI reference (Section 4.1) may be a relative-path reference, in which case the first path segment cannot contain a colon (":") character.
Thus, the :
is permitted in relative paths (which is what we're dealing with here), but simply not in the first component.
Consider:
let string = "foo:bar/baz:qux"
print(string.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed)!)
That will, in conformance with RFC 3986, percent encode the :
in the first component, but allow it unencoded in subsequent components:
foo%3Abar/baz:qux
This character set is not percent encoding solely on the basis of what characters are in the set, but is actually applying RFC 3986's relative path logic. But as Cœur said, if you need, you can bypass this logic by building your own character set with the same allowed characters as .urlPathAllowed
, and that new character set will not apply this RFC 3986 logic.
Upvotes: 10
Reputation: 38727
Apparently there is some undocumented magic done by addingPercentEncoding
when the CharacterSet used as reference is an underlying NSCharacterSet class.
So to workaround this magic, you need to make your CharacterSet a pure Swift object. To do so, I'll create a copy (thanks Martin R!), so that the evil magic is gone:
let myString = "info:hello world"
let csCopy = CharacterSet(bitmapRepresentation: CharacterSet.urlPathAllowed.bitmapRepresentation)
let escapedString = myString.addingPercentEncoding(withAllowedCharacters: csCopy)!
//always "info:hello%20world"
print(escapedString)
As an extension:
extension String {
func safeAddingPercentEncoding(withAllowedCharacters allowedCharacters: CharacterSet) -> String? {
// using a copy to workaround magic: https://stackoverflow.com/q/44754996/1033581
let allowedCharacters = CharacterSet(bitmapRepresentation: allowedCharacters.bitmapRepresentation)
return addingPercentEncoding(withAllowedCharacters: allowedCharacters)
}
}
Upvotes: 12