Synchronous HTTP GET in Swift

Posted on Sat 09 June 2018 in Swift

In the last few posts, we've seen how to print, get arguments from the command line, and how to get lines of keyboard input and implement a REPL (read, evaluate, print loop).

The particular command-line tools I want to build are for interacting with MicroDB, an online distributed tag-based database that I'm developing.

In order to interact with MicroDB, we're going to need to be able to perform various HTTP operations---specifically GET, PUT, POST and DELETE. So I wanted to explore how to do those in Swift. The code below illustrates one of many ways to perform an HTTP GET in Swift, based on information from these four web resources.

// get-synchronous.swift  [Swift 4.1, Xcode 9.3.1, 2018-06-09]
//
// Method 1 for getting a URL in Swift

// Simple, Synchronous URL get.
//
// Based on https://www.hackingwithswift.com/example-code/strings/how-to-load-a-string-from-a-website-url
//   and https://developer.apple.com/documentation/foundation/nsstring/1411946-addingpercentencoding
//   and https://developer.apple.com/documentation/foundation/characterset
//   and https://stackoverflow.com/questions/32064754/how-to-use-stringbyaddingpercentencodingwithallowedcharacters-for-a-url-in-swi


import Foundation

let path = "njr.radcliffe0.com/album:solid air (john martyn)/Title"
let enc = path.addingPercentEncoding(withAllowedCharacters: CharacterSet.urlPathAllowed)
let urlString = "https://" + enc!  // don't %-encode "https://"

if let url = URL(string: urlString) {
    do {
        let contents = try String(contentsOf: url)
        print(contents)
    } catch {
        print("*** Contents could not be loaded from" + urlString + ".")
        }
} else {
    print("*** Bad URL:" + urlString + ".")
}

Let's go through it:

MicroDB. In MicroDB, information is stored as an association between a user, a unique (meaningful) identifer, a tag name and a value. The user is identified by a domain. I have an instance of MicroDB running at http://njr.radcliffe0.com, and information about John Martyn's album Solid Air stored on the identifier album:solid air (john martyn). One of the bits of information I have about the album is its name, which is stored in a tag called Title. So if I perform an HTTP GET to https://njr.radcliffe0.com with the path album:solid air (john martyn)/solid air, I should get back the title of the album. If we try this with curl, indeed we see that (percent-encoding spaces as %20):

$ curl 'https://njr.radcliffe0.com/album:solid%20air%20(john%20martyn)/Title'
Solid Air

Percent Encoding. In Swift (at least with our chosen way of fetching a URL), we also have to percent encode the URL path ourselves. The method addPercentEncoding on strings can do this, but we need to specify which characters not to encode by passing an arugument to the method. The argument name is withAllowedCharacters and the various standard character sets are available in Foundation as documented on this page. urlPathAllowed is the list we need for a URL path.

Optionals. We then for the full URL by prepending the percent-encoded path with https:// using simple string concatenation (+ in Swift). But notice the exclamation mark ! on the end of enc. That's because addingPercentEncoding returns a Swift optional, i.e. a value that might be nil. The documentation specifically says this:

Returns the encoded string, or nil if the transformation is not possible.

(I'm not sure in exactly what circumstances the transformation wouldn't be possible, but presumably there are, or might at some point, be some.)

Unwrapping Optionals By following variable with !, we are telling Swift that even though its contents could be nil, in this case they are not. This is called "unwrapping an optional". In this case, we know the value will not be nil because we're giving it string can obviously be percent-encoded (all the characters are ASCII!), so it's fine to force it. If we did turn out to be wrong, however, this would cause the Swift executable to crash. Here's a simple interaction with the Swift REPL that illustrates that:

$ swift
Welcome to Apple Swift version 4.1 (swiftlang-902.0.48 clang-902.0.39.1). Type :help for assistance.
   1> let a:Int? = nil
a: Int? = nil
2> a!
Fatal error: Unexpectedly found nil while unwrapping an Optional value
2018-06-02 21:52:44.040840+0100 repl_swift[22405:1982301] Fatal error: Unexpectedly found nil while unwrapping an Optional value
Execution interrupted. Enter code to recover and continue.
Enter LLDB commands to investigate (type :help for assistance.)
Process 22405 stopped
* thread #1, queue = 'com.apple.main-thread', stop reason = EXC_BAD_INSTRUCTION (code=EXC_I386_INVOP, subcode=0x0)
    frame #0: 0x00000001011d5c50 libswiftCore.dylib`function signature specialization <Arg[2] = Dead, Arg[3] = Dead> of Swift._fatalErrorMessage(Swift.StaticString, Swift.StaticString, file: Swift.StaticString, line: Swift.UInt, flags: Swift.UInt32) -> Swift.Never + 96
libswiftCore.dylib`function signature specialization <Arg[2] = Dead, Arg[3] = Dead> of Swift._fatalErrorMessage(Swift.StaticString, Swift.StaticString, file: Swift.StaticString, line: Swift.UInt, flags: Swift.UInt32) -> Swift.Never:
->  0x1011d5c50 <+96>: ud2
    0x1011d5c52 <+98>: nopw   %cs:(%rax,%rax)

libswiftCore.dylib`function signature specialization <Arg[0] = Owned To Guaranteed and Exploded, Arg[1] = Exploded> of Swift.String._compareASCII(Swift.String) -> Swift.Int:
    0x1011d5c60 <+0>:  pushq  %rbp
    0x1011d5c61 <+1>:  movq   %rsp, %rbp
Target 0: (repl_swift) stopped.
3> ^D

Here we made the variable a be an optional integer by specifying its type as Int? and assigned its value as nil. We then try to forcibly unwrap it using a!, and as we see a crash ensues.

The if...else statement. Like the while statement we saw previously, the if...else statment should look fairly familiar if you know any C-like language. Again, the condition does not require parentheses, and again this one is using a let---assignment of a constant. The if is protecting against the possibility the that URL object that we create with with URL(string:urlString is nil, which it will be if the urlString is not a valid URL.

The do...try...catch statement. Assuming the URL is valid, we try to fetch it, and we do this in a do...ctry...atch block, which is Swift's way of allowing us to handle exceptions.1 We'll get such an exception if the URL can't be fetched.

Performing the HTTP GET. In this case, we attempt a synchronous HTTP GET simply by initializing a string using contentsOf: url as an initializer. This causes Swift to (try to) get the URL contents. Notice the try which indicates that this is the statment inside the do...catch block that might throw an exception.

That's it!


  1. In this case, we're catching any exception that might be thrown, but we could also be more specific and catch particular kinds of exceptions; I imagine we'll see this in later posts.