URLComponents and Thoughts on API Design
TLDR; There are ways to generate a runtime crash while using URLComponents
that are not obvious. I hope to show some reasons why this makes for a bad developer experience and to present some ideas for how this could have been avoided.
A URLQueryItem adding Utility
Let’s say you want to write a utility to add a query parameter to a url.
Broadly speaking, there are two approaches to this:
- Get a
String
that looks like aURL
. Modify thatString
associated with theURL
. This is wrong. Please do not do this. - Use
URLComponents
. This is a better approach.
Let’s assume you take the first approach.
Writing this is very simple. We can use a case-less enum with a static function that takes an array of URLQueryItem
s and a URL
to attach them to. (this could also be done as an extension of URL
but I like this separation more. It’s just personal style, both are valid approaches.
Notice the two guard
statements:
The first, guard var components = URLComponents(url: url ...
, is there because it is possible to have a URL
that cannot be converted into URLComponents
. This is weird but apparently you can have a valid URL
that is actually malformed… This is a topic for another day.
The second, guard let modifiedURL = components.url
, is also very weird. I was curious about how you could have URLComponents
that couldn’t be turned into a URL
. Isn’t that the whole point of having URLComponents
in the first place?
Breaking URLComponents
As it turns out. I was not able to get components.url
to return nil
. I was, however, able to find a three ways of causing a runtime exception!
First crash!
Let’s take a quick quiz:
Which of these snippets will cause a runtime exception?
A. Empty Components
B. Missing host, valid path
C. Empty host, valid path
D. Missing host, strange path
E. Empty host, strange path
Answer:
A: No
B: No
C: No
D: No
E: Yes! If you have a non-empty String
for the host parameter and a path parameter that does not begin with a /
you will crash at runtime. This is very obviously a bad thing.
Second Crash!
Time for another quiz!
Which of these snippets will cause a runtime exception?
A: Empty Scheme
B: Scheme with letters and numbers
C: Scheme with numbers then letters
D: Familiar Looking Scheme
Answers:
A: Yes — cannot pass in an empty string
B: No — can pass a string that ends with `numbers`
C: Yes — cannot pass a string that starts with `numbers`
D: Yes — cannot pass a string with special characters
Third Crash!
Last quiz, I promise.
Which of these snippets will cause a runtime exception?
A: Port zero
B: Port negative zero
C: Port huge number
D: Port negative number
Answer:
Only D!
Summary of Crash Causes
So basically you will crash while trying to create a URL
from URLComponents
if you have the following:
- host / path mismatch
- scheme starts with integers or contains special characters
- negative port number
This is a problem to me as a user of the URLComponents
api.
How could this be improved?
In fairness, the documentation for URLComponents
mentions the crash involving the scheme and the crash involving the port number. It does not however, mention anything about the crash from inconsistent host and path values.
Also, credit where it is due; a URLComponents
object is extremely flexible in what it accepts and turns into a URL
. For instance, the following snippet will not crash. It will produce a bizarre but valid URL
:
I think a couple improvements could be added.
- Port should be a
UInt
. If you are writing an API that crashes when a negative value is passed in, you are using the wrong type. Furthermore, it’s possible to write a dependent type that can protect against the scheme and port/host mismatch crashes by demanding stronger types for those inputs.
2. Any method with known runtime exceptions should be able to capture and handle those exceptions.
3. Any property with that can except a value that will cause an exception should either take different type or be private set and expose a throwing method for setting the value.
Thanks for reading! Hopefully you get something out of this and maybe avoid a crash or two!