The behavior I’ll describe is the same with the Swift API URL and the Objective-C API NSURL. I still use Objective-C in my own apps and will use it in this blog post; regardless of how you feel about that older language, one of its indisputable advantages is that searching the web for an Objective-C API such as NSURL is vastly more productive than searching for a Swift API such as URL! It turns out that namespaces matter outside of source code.
The Apple developer documentation linked above includes an important warning:
For apps linked on or after iOS 17 and aligned OS versions,
NSURLparsing has updated from the obsolete RFC 1738/1808 parsing to the same RFC 3986 parsing asNSURLComponents. This unifies the parsing behaviors of theNSURLandNSURLComponentsAPIs. Now,NSURLautomatically percent- and IDNA-encodes invalid characters to help create a valid URL.To check if a
URLStringis strictly valid according to the RFC, use the new[NSURL URLWithString:method. This method leaves all characters as they are and returnsURLString encodingInvalidCharacters: NO] nilifURLStringis explicitly invalid.
Among the aligned OS versions are macOS 14 Sonoma, released in 2023 along with iOS 17.
Let’s consider two example URL strings:
https://example.org?url=https%3A%2F%2Fexample.org%3Ffoo(0)%3Dbar https://example.org?url=https%3A%2F%2Fexample.org%3Ffoo[0]%3Dbar These two are identical except that the second replaces the left and right parentheses, which are allowed in a URL query, with left and right brackets, which are disallowed in a URL query. Consequently, [NSURL URLWithString: returns non-null in the first case and null in the second case, as expected.
The older, simpler API [NSURL URLWithString: behaves the same as [NSURL URLWithString: when your code is compiled with the iOS 17 or macOS 14 SDK. So much for backward compatibility! [NSURL URLWithString: continues to work fine with example 1, leaving the URL string untouched, but it mangles example 2:
https://example.org?url=https%253A%252F%252Fexample.org%253Ffoo%5B0%5D%253Dbar
Notice that the brackets are encoded as %5B and %5D, which is good, but the preexisting % characters are also encoded as %25, thereby transforming %3A (an encoded : character) into %253A, which is bad, indeed bonkers! The % characters did not need to be encoded, because they are already valid characters in a URL query.
When you call [NSURL URLWithString: on iOS 16 or macOS 13 and earlier, before this change of behavior was implemented, the expected, sane result is returned:
https://example.org?url=https%3A%2F%2Fexample.org%3Ffoo%5B0%5D%3Dbar
In other words, the invalid bracket characters are encoded, and everything else that was already valid in the URL remains the same.
My app Link Unshortener uses [NSURL URLWithString:, which is how I discovered the unfortunate double-encoding of URL characters. The real-world example containing invalid bracket characters in the query was a Facebook URL.
I’ve filed a bug report in Apple Feedback Assistant: “URL/NSURL double-encodes characters unnecessarily” (FB20439045).
As far as I’m aware, a good workaround for this bug does not exist. The only reliable “solution” would be to abandon the Apple NSURL API and write my own URL parser, a prospect that I do not relish, especially just to handle a corner case.