URL(string:) behavior changed with Xcode 15.0 Beta 5

With Xcode 14.3.1 (Swift 5.8.1) I was able to convert URL(string: "127.0.0.1:8000/test") into a valid URL. With Xcode 15.0 Beta 5 (Swift 5.9) the same initializer returns nil. Is this a new behavior or a bug? Does Swift 5.9 already use a pre version of the new open source Foundation package, which already includes URL?

I wonder if this is really a valid URL.
It is not defined what kind of URL is used, e.g.

URL(string: "http://127.0.0.1:8000/test")

The documentation for URL is not really helpful but at NSURL " init(string:)" is some more information:

URLString

The URL string with which to initialize the NSURL object. This URL string must conform to URL format as described in RFC 2396, and must not be nil. This method parses URLString according to RFCs 1738 and 1808.

Just out of curiosity - how were you using the resulting URL value before? Because there is no scheme, there is no way to know what the string means, and indeed the reported components are not what a human would expect:

let url = URL(string: "127.0.0.1:8000/test")!

url.host // nil
url.port // nil
url.path // "127.0.0.1:8000/test"

You also can't make a request to it:

// ❌ throws error: "Unsupported URL"
for try await l in URL(string: "127.0.0.1:8000/test")!.lines {
  print(l)
}

Don't get me wrong - compatibility is very important and behaviour changes need to be made very carefully. But I also wonder what was left that you could do with these URL values.

1 Like

Note to avoid potential confusion that URL is a type in a framework, and so its implementation ships in the operating system. So any change in this behavior would be related to the OS you are running the binary on (i.e. macOS Sonoma), not the version of Swift or Xcode you have.

(an exception to this, which I don't think applies here, is if the initializer were inlinable, in which case it would be related to the version of the SDK you are using, which is tied to Xcode)

6 Likes

Part of the confusion is the the change was originally noted in the Xcode 15 beta 1 release notes rather than any particular OS.

Fixed: For apps linked on or after iOS 17 and aligned OS versions, URL parsing has updated from the obsolete RFC 1738/1808 parsing to the same RFC 3986 parsing as URLComponents. This unifies the parsing behaviors of the URLand URLComponents APIs. Now, URL automatically percent- or 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 URL(string: urlString, encodingInvalidCharacters: false) initializer. This init leaves all characters as they are and will return nil if urlString is explicitly invalid. (93368104)

Granted, Apple doesn't have a good alternate place to put changes like this without duplicating it in every OS' release notes (which has happened), but some confusion is likely when it shows up in the Xcode release notes itself.

5 Likes

So...passing invalid strings on iOS15~16 will always succeed once encodingInvalidCharacters is only available on iOS17?

I see that in Xcode 15.0.1 passing invalid strings to iOS15~16 has the original behaviour: it will fail when passing invalid characters.

1 Like

Yes, this behavior isn't part of the compiler but the version of the SDK you're running on. Definitely an instance where you need to keep your old escaping logic around conditionally. But it is confusing since the compiler doesn't tell you anything different with an older deployment target. You could also use the new URL(string:encodingInvalidCharacters:) init on iOS 17 to match the older behavior.

1 Like

If you want to more closely match the previous encoding behavior, you can create your URL using CoreFoundation APIs, which haven't been updated to use the new encoding scheme:

let cfurl = CFURLCreateWithString(nil, string as CFString, nil)
let url = cfurl as URL?

Is that different from turning off the encoding behavior in the new initializer?

Interestingly enough it is. When linking against iOS 17+ the underlying implementation of URL(string:) calls into URLComponents to convert the string to a URL, which uses RFC 3986 for parsing regardless of the encoding parameter’s value.

Prior to this, the implementation simply called into CFURL to create an underlying URL object, which uses the (obsolete) RFC 1738/1808 for parsing.

All that to say, if you want the old parsing behavior (RFC 1738/1808) use CFURLCreateWithString as it still uses the legacy parsing strategy

What I meant was, does the new URL(string:encodingInvalidCharacters:) initializer match the old behavior?

It does not. This method, when linking against iOS 17.0 or above, will use RFC 3986 for its URL parsing regardless of the valid you provide for the second parameter to this initializer.

Try the following:

  1. Create a sample project with Xcode 15
  2. Using the CFURL APIs, try and initialize a URL with the string below. (It should succeed)
  3. Now create a URL using the initializer above with encodingInvalidCharacters set to false (it should fail)

Sample URL string: ”https://www.website.com?q=[somevalue]”