URLComponents and Thoughts on API Design

Joe Susnick
3 min readMar 10, 2019

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:

  1. Get a String that looks like a URL. Modify that String associated with the URL. This is wrong. Please do not do this.
  2. 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 URLQueryItems 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.

  1. 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!

--

--