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 parsesURLString
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.
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)
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 asURLComponents
. This unifies the parsing behaviors of theURL
andURLComponents
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 newURL(string: urlString, encodingInvalidCharacters: false)
initializer. This init leaves all characters as they are and will returnnil
ifurlString
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.
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.
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.
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:
- Create a sample project with Xcode 15
- Using the CFURL APIs, try and initialize a URL with the string below. (It should succeed)
- Now create a URL using the initializer above with
encodingInvalidCharacters
set tofalse
(it should fail)
Sample URL string: ”https://www.website.com?q=[somevalue]”