.urlQueryAllowed do not encode slashes (/) because slashes are valid query delimiters under RFC 3986. To escape slashes and other reserved characters safely, assemble your request using URLComponents and URLQueryItem, or apply addingPercentEncoding(withAllowedCharacters:) using .urlHostAllowed or a custom CharacterSet.Why this happens
When encoding strings to pass as URL parameters or path segments, developers often find that characters like /, &, or = remain unencoded. In legacy Objective-C and early Swift code, calling methods like stringByAddingPercentEscapesUsingEncoding left slashes untouched. In modern Swift, calling addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) produces the same result: a string like "folder/item" remains "folder/item" rather than becoming "folder%2Fitem".
This behavior occurs because Foundation’s predefined CharacterSet instances are based on RFC 3986 section specifications. Under RFC 3986, the forward slash (/) and question mark (?) are allowed characters within a query component. The system treats .urlQueryAllowed as the set of all characters that may legally appear anywhere inside the entire query string, not the set of characters allowed within an individual key or value.
When you interpolate a raw string containing / directly into a query string or path segment, HTTP servers and parsers treat the slash as a sub-path delimiter or structural separator rather than literal text. If your parameter values contain slashes, spaces, or symbols, you must either let Foundation construct the query using dedicated component types or explicitly remove structural delimiters from your allowed character set.
How to fix it
1. Construct URLs with URLComponents and URLQueryItem (Recommended)
The safest way to build URLs with parameters containing slashes, spaces, or symbols is to use URLComponents together with URLQueryItem. When you assign items to queryItems, Foundation automatically handles the percent-encoding for each key and value, ensuring structural characters do not break the URL structure.
import Foundation
func makeSearchURL(searchTerm: String) -> URL?
var components = URLComponents()
components.scheme = "https"
components.host = "api.example.com"
components.path = "/v1/search"
// URLQueryItem automatically percent-encodes values, escaping slashes and spaces
components.queryItems = [
URLQueryItem(name: "filter", value: searchTerm),
URLQueryItem(name: "format", value: "json")
]
return components.url
}
if let url = makeSearchURL(searchTerm: "categories/electronics") {
print(url.absoluteString)
// Prints: https://api.example.com/v1/search?filter=categories%2Felectronics&format=json
}
Using URLComponents eliminates manual string formatting errors and prevents characters like & and = in user input from corrupting your query parameters.
2. Use addingPercentEncoding with .urlHostAllowed
If you need to encode a standalone string where slashes must be converted to %2F, you can use the .urlHostAllowed character set. In RFC 3986, hostnames cannot contain forward slashes, so .urlHostAllowed does not include /.
import Foundation
let originalString = "reports/2026/q1"
if let encodedString = originalString.addingPercentEncoding(withAllowedCharacters: .urlHostAllowed) {
print(encodedString)
// Prints: reports%2F2026%2Fq1
}
While .urlHostAllowed escapes slashes, it also excludes characters like :, @, and ?. Verify that this set fits your endpoint’s expectations before applying it broadly.
3. Create a custom CharacterSet for strict parameter encoding
Predefined sets such as .urlQueryAllowed leave characters like /, +, &, and = unencoded. When formatting query parameters manually or interfacing with strict backend APIs, define a custom set by removing reserved characters from .urlQueryAllowed:
import Foundation
extension CharacterSet {
/// A character set conforming to RFC 3986 for individual query values.
static let urlQueryValueAllowed: CharacterSet = {
// Start with the standard query set
var set = CharacterSet.urlQueryAllowed
// Remove sub-delimiters and reserved characters that break key=value parsing
set.remove(charactersIn: "!*'();:@&=+$,/?%#[]")
return set
}()
}
let rawParam = "user/data=true&active=yes"
if let escapedParam = rawParam.addingPercentEncoding(withAllowedCharacters: .urlQueryValueAllowed) {
print(escapedParam)
// Prints: user%2Fdata%3Dtrue%26active%3Dyes
}
This approach ensures that your manual string interpolations will not corrupt URL structure when sending data through custom protocols or web sockets.
4. Encode path components using appendingPathComponent
If the slash represents part of a resource filename rather than a hierarchy, avoid concatenating it with string operators (+). Instead, pass the raw string to Foundation’s path manipulation methods:
import Foundation
let baseURL = URL(string: "https://api.example.com/files")!
let resourceName = "document/archive.pdf"
// In modern Swift (iOS 16+ / macOS 13+):
let modernURL = baseURL.appending(component: resourceName)
print(modernURL.absoluteString)
// Prints: https://api.example.com/files/document%2Farchive.pdf
// For backwards compatibility across older OS releases:
let legacyURL = baseURL.appendingPathComponent(resourceName)
print(legacyURL.absoluteString)
// Prints: https://api.example.com/files/document%2Farchive.pdf
Foundation’s appending(component:) method automatically escapes forward slashes because it treats the argument as a single component rather than a sub-path.
How to prevent it
- Never build query strings with String interpolation: Avoid writing
"\(baseURL)?query=\(param)". UseURLComponentsandURLQueryItemto delegate encoding to the OS. - Differentiate path components from sub-paths: Use
baseURL.appending(path: ...)if your string contains intentional directory separators, andbaseURL.appending(component: ...)if slashes are literal text. - Audit predefined character sets: Remember that
.urlQueryAllowed,.urlPathAllowed, and.urlFragmentAllowedare intended for entire components, not individual key-value pairs. - Verify round-trip decoding: Add unit tests that decode encoded URLs using
removingPercentEncodingto ensure strings return to their original form without data loss.
FAQ
Why doesn’t .urlQueryAllowed encode the ampersand (&) or equal sign (=)?
According to RFC 3986, & and = are valid syntax characters within a query component used to separate multiple key-value pairs. Because .urlQueryAllowed defines characters allowed anywhere in the query string as a whole, it treats them as safe and does not encode them.
What is the difference between appending(path:) and appending(component:)?
appending(path:) parses forward slashes as structural directory separators, adding multiple path segments to the URL. In contrast, appending(component:) treats the entire argument as a single path segment and percent-encodes any slashes inside it into %2F.
How do I decode a percent-encoded string back into standard text?
Call the removingPercentEncoding property on your String. This property returns an optional String? containing the decoded characters, or nil if the string contains an invalid or malformed percent-escape sequence.
Parts of this guide are adapted from content licensed under CC BY-SA 3.0; this article is shared under the same license.
