swiftfoundationnsurlcomponents

URLComponents queryItems losing percent encoding when mutated


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:

enter image description here


Solution

  • 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)
    }