Jeff Johnson (My apps, PayPal.Me, Mastodon)

URL/NSURL double-encodes characters unnecessarily

September 30 2025

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, NSURL parsing has updated from the obsolete RFC 1738/1808 parsing to the same RFC 3986 parsing as NSURLComponents. This unifies the parsing behaviors of the NSURL and NSURLComponents APIs. Now, NSURL automatically percent- and IDNA-encodes invalid characters to help create a valid URL.

To check if a URLString is strictly valid according to the RFC, use the new [NSURL URLWithString:URLString encodingInvalidCharacters:NO] method. This method leaves all characters as they are and returns nil if URLString is 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:

  1. https://example.org?url=https%3A%2F%2Fexample.org%3Ffoo(0)%3Dbar
  2. 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:URLString encodingInvalidCharacters:NO] returns non-null in the first case and null in the second case, as expected.

The older, simpler API [NSURL URLWithString:URLString] behaves the same as [NSURL URLWithString:URLString encodingInvalidCharacters:YES] when your code is compiled with the iOS 17 or macOS 14 SDK. So much for backward compatibility! [NSURL URLWithString:URLString] 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:URLString] 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:URLString], 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.

Jeff Johnson (My apps, PayPal.Me, Mastodon)