Using the Swift Server API 0.1.0

The Swift Server APIs Project is an effort the provide a common HTTP implementation to be used in the various Swift application server frameworks (Kitura, Perfect and so on). The ARI shows you how to use it on either macOS or Linux.

HTTP Server API 0.1.0

On October 2nd, 2017, Chris Bailey of IBM announced the Swift Server API v0.1.0:

This is a small tutorial on how to use the API to write a small HTTP endpoint in Swift.

HTTP 0.1.0 is the first result of the Server API working group and is based on a proposal by Johannes Weiß of Apple. It provides a very basic API to implement an HTTP server in Swift. Don’t expect a full fledged web framework, it provides just the basics to handle HTTP requests.

This post tries to explain how to actually use it. Before we start, the project provides nice Jazzy generated API documentation, you may want to keep it open.

Step 0: Requirements

The HTTP 0.1.0 API is shipped as a Swift Package Manager 4 (SPM) package. This means you need to have Swift 4 installed, e.g. on macOS using Xcode 9. To play on Linux you can either download Swift 4 tarballs, or just use the official Docker image (you can also use my helje5/swift-dev image, which comes w/ Emacs, etc).

When using a Docker image, don’t forget to start it with a port exposed, like so: docker run -p 1337:1337 -it --name sssfun helje5/swift-dev /bin/bash

We start out working in the shell (Terminal.app on macOS) - but if you want, you can switch to Xcode soon.

Step 1: Setup Hello World Package

OK, lets starts with a very simple Hello World service, which you can hit using http://localhost:1337/hello.

Once you have Swift 4, you can start creating the Swift package. First create a directory, then initialize it:

mkdir sssfun && cd sssfun
swift package init --type executable
  Creating executable package: sssfun
  Creating Package.swift
  Creating README.md
  Creating .gitignore
  Creating Sources/
  Creating Sources/sssfun/main.swift
  Creating Tests/

This creates a basic Hello World tool, you can build and run it:

swift build
  Compile Swift Module 'sssfun' (1 sources)
  Linking ./.build/x86_64-unknown-linux/debug/sssfun
  
.build/*/debug/sssfun
  Hello, world!

OK, great. We have a working Swift package. If you want to use Xcode, Swift Package Manager can now generate an Xcode project for you:

swift package generate-xcodeproj
  generated: ./sssfun.xcodeproj

open sssfun.xcodeproj # and Xcode should open

Within Xcode, make sure you select the right scheme when trying to build and run the tool!

Step 2: Add HTTP 0.1.0 as a Dependency

Within our sssfun package, you’ll find the Package.swift file. In there we need to tell SPM that we want to use the HTTP API package. Open the file in either Xcode or Emacs, and add the dependency lines as shown:

let package = Package(
    name: "sssfun",
    dependencies: [
        .package(url: "https://github.com/swift-server/http", 
                 from: "0.1.0")
    ],
    targets: [
        .target(
            name: "sssfun",
            dependencies: [ "HTTP" ])
    ]
)

This tells SPM that we want to use the HTTP-API (living on GitHub). And then we tell it that our sssfun tool, needs the HTTP library within that package. We also need to specify the API version (0.1.0 in this case).

Call swift build again, it should now download the dependency and build it:

swift build
  Fetching https://github.com/swift-server/http
  Cloning https://github.com/swift-server/http
  Resolving https://github.com/swift-server/http at 0.1.0
  Compile CHTTPParser http_parser.c
  Compile Swift Module 'HTTP' (11 sources)
  Compile Swift Module 'sssfun' (1 sources)
  Linking ./.build/x86_64-unknown-linux/debug/sssfun

Xcode users: Everytime you change the Package.swift you need to recreate the Xcode project using: swift package generate-xcodeproj

Excellent. Now we are getting somewhere!

Lets create a new file - we can call it HelloHTTP.swift - in the Sources/sssfun folder. You can do this in Emacs/vi (emacs Sources/sssfun/HelloHTTP.swift), or in Xcode, however you like:

// File: Sources/sssfun/HelloHTTP.swift
import Foundation
import HTTP
 
func hello(request: HTTPRequest, response: HTTPResponseWriter ) 
       -> HTTPBodyProcessing 
{        
    response.writeHeader(status: .ok) 
    response.writeBody("Hello, World!\n") 
    response.done() 
    return .discardBody 
} 

This is our HTTP request handling function. It gets passed in the HTTPRequest and the HTTPResponseWriter. It has to return instructions how the API is supposed to handle request content (e.g. a file upload). In this case, we just drop any content that is sent (we return .discardBody).

The HTTPRequest has properties for the method (e.g. GET or POST), the target (the URL the request is working on, e.g. /hello), HTTP request headers (e.g. User-Agent), as well as the HTTP protocol version.

The HTTPResponseWriter we get allows us to send data back to the client, i.e. deliver the response content. In our case we just say .ok (HTTP 200 status code), write out some text, and then mark the response as .done().

Very nice. So this is the Hello World of HTTP API. But there is one piece missing, we need to setup the server running our handler function. To do that, we are going to edit Sources/sssfun/main.swift. Right now, this contains the print("Hello World") as generated by SPM. Replace that code with the HTTP 0.1.0 sample server:

// File: Sources/sssfun/main.swift
import Foundation
import HTTP

let server = HTTPServer()
do {
    try server.start(port: 1337, handler: hello)
}
catch {
    print("failed to start server:", error)
    exit(42)
}

RunLoop.current.run()

We create the server object, start it at port 1337. Then we start the Runloop, which is responsible for coordinating the access to the server sockets. That run() call will never complete (stop the server in Xcode, or use Ctrl-C in the shell).

That’s it. Call swift build and re-run the tool (or just press Run in Xcode):

swift build
  Compile Swift Module 'sssfun' (2 sources)
  Linking ./.build/x86_64-unknown-linux/debug/sssfun

.build/*/debug/sssfun
  Started server on port 1337 with 4 serial queues 
  of each type and 8 accept sockets

The server is now running and accepting requests. You can hit it using http://localhost:1337/hello and it should display Hello World:

Xcode users: If you see stops in the Xcode debugger related to SIGPIPE, create a file ~/.lldbinit with this content:
process handle SIGPIPE -n true -p true -s false. Restart Xcode.

Step 3: Cows. Lots of them. 🐄🐂🐄

All this is nice, but pretty boring, isn’t it? Let’s add some beef! We are going to use the Swift cows module.

Reopen the Package.swift file, and add the cows package and the cows module (Xcode users rerun swift package generate-xcodeproj after doing this):

let package = Package(
    name: "sssfun",
    dependencies: [
        .package(url: "https://github.com/swift-server/http",
                 from: "0.1.0"),
        .package(url: "https://github.com/AlwaysRightInstitute/cows.git",
                 from: "1.0.0")
    ],
    targets: [
        .target(
            name: "sssfun",
            dependencies: [ "HTTP", "cows" ]),
    ]
)

Next we’ll add beef to our HelloHTTP.swift endpoint in the Sources/sssfun folder.

// File: Sources/sssfun/HelloHTTP.swift
import Foundation
import HTTP
import cows

func hello(request: HTTPRequest, response: HTTPResponseWriter )
     -> HTTPBodyProcessing
{
    response.writeHeader(status: .ok, 
                         headers: [ "Content-Type": "text/html" ])
    response.writeBody(
        """
        <h1>cows</h1>
        <center><pre>\(vaca().htmlEscaped)</pre></center>
        """
    )
    response.done()
    return .discardBody
}

extension String {
    var htmlEscaped : String {
        return map { escapeMap[$0] ?? String($0) }.reduce("", +)
    }
}
let escapeMap : [ Character : String ] = [
    "<" : "&lt;", ">": "&gt;", "&": "&amp;", "\"": "&quot;"
]

This is a little more difficult and shows that HTTP API only provides very basic stuff. It doesn’t deal with HTML or anything - that would be the responsibility of a higher level framework (like Kitura, Vapor, etc). For example we need to escape some characters to make the cows render as proper HTML.

Build and re-run the thing, then hit http://localhost:1337/hello and reload the page as often as you like. You should get plenty of cows delivered to your browser.

Step 4: Reinventing Google

Sure, random cows are just great and always carry a surprise. But lets add a way to search for specific cows. Again, you’ll see that you need to do some stuff manually, which a higher level framework would usually do for you. But we can get it done w/ a little code.

In the handler we add an HTML form, we add helper functions to decode URL query parameters, and we add the code to search for cows once we got a query string.

// File: Sources/sssfun/HelloHTTP.swift
import Foundation
import HTTP
import cows

func hello(request: HTTPRequest, response: HTTPResponseWriter )
     -> HTTPBodyProcessing
{
    let q   = request[query: "q"]?.lowercased() ?? ""
    let cow = q.isEmpty
        ? vaca() // random cow
        : allCows.first(where: { $0.lowercased().contains(q) }) 
          ?? "No such cow"
    
    response.writeHeader(status: .ok,
                         headers: [ "Content-Type": "text/html" ])
    response.writeBody(
        """
        <center>
          <form action="/hello/" method="get">
            Find Beef: <input name="q" placeholder="e.g. 'moon'">
            <a href="/hello/">[random]</a>
          </form>
          <pre>\(cow.htmlEscaped)</pre>
        </center>
        """
    )
    response.done()
    return .discardBody
}

// That can be put into a separate Helpers.swift file:

extension HTTPRequest {
    
    subscript(query q: String) -> String? {
        return urlQueryParameters.first(where: { $0.0 == q })?.1
    }
    
    var urlQueryParameters : [ (String, String) ] {
        return URL(string: target)?.query?.components(separatedBy: "&")
            .map {
                let pair = $0.split(separator: "=", maxSplits: 1).map(String.init)
                return ( pair.first?.removingURLQueryParameterEncoding ?? "",
                         pair.last? .removingURLQueryParameterEncoding ?? "" )
            } ?? []
    }
}

extension String {
    var htmlEscaped : String {
        return map { escapeMap[$0] ?? String($0) }.reduce("", +)
    }
    var removingURLQueryParameterEncoding : String {
        return replacingOccurrences(of: "+", with: " ")
               .removingPercentEncoding
               ?? "😰"
    }
}
let escapeMap : [ Character : String ] = [
  "<" : "&lt;", ">": "&gt;", "&": "&amp;", "\"": "&quot;"
]

Build and re-run the thing, then hit http://localhost:1337/hello. Enter some query string, e.g. ‘moon’, ‘lator’, ‘trouble’ or ‘night’.

Very well. We built a very basic HTTP service which competes with Google on search, and also delivers pretty cows.

Bonus: Use Apache to host your HTTP endpoint

So you are happy with our cows web service and want to deploy it on production. But the 0.1.0 API server may not be quite there yet, and you really want HTTP/2, HTTPS and NTLM authentication for sure.

Fortunately the ARI hacked up an implementation of the very same Swift API which allows you to run the exact same code in the “The most popular web server on the Internet since April 1996”: Apache httpd. We are going to use an Apache module called mod_swift and an implementation of the HTTP API 0.1.0 for mod_swift.

For the following we assume that you are on macOS w/ Homebrew. (The same should work fine on Linux, just checkout the mod_swift Installation to get going, and please let us know if you run into issues).

If you don’t have Homebrew yet, get it at: brew.sh (installation is a one liner). Then install mod_swift, and maybe httpd w/ HTTP/2 support and threading:

brew reinstall httpd --with-mpm-event --with-http2

brew install mod_swift
==> Installing mod_swift from modswift/mod_swift
🍺  /usr/local/Cellar/mod_swift/0.8.10: 25 files, 68.5KB, built in 5 seconds

To check whether it worked, call swift apache validate, like so:

swift apache validate
  The Swift Apache build environment looks sound.

Neat. The next step is to replace the Sources/sssfun/main.swift, which contains the API 0.1.0 test server w/ Apache. Note that we keep using the HelloHTTP.swift w/o any changes! We also need to clean the package:

rm -rf .build Package.resolved
rm Sources/sssfun/main.swift

Create a new file ApacheMain.swift in Sources/sssfun:

import Foundation
import HTTP

@_cdecl("ApacheMain")
public func ApacheMain(cmd: OpaquePointer) {
  let app = HTTP.apache(cmd, name: "mods_httpapi")
  app.use("/hello", hello)
}

Before we can build this, we need to replace the API dependency in Package.swift. Note that the module name is still the same, we just change the package providing that HTTP module. Replace this:

.package(url: "https://github.com/swift-server/http",
         from: "0.1.0"),

with:

.package(url: "https://github.com/modswift/http.git", 
         .branch("implementation/mod_swift")),

Now call swift apache build, which will create the Apache module for us:

swift apache build
  Fetching https://github.com/modswift/http.git
  Fetching https://github.com/AlwaysRightInstitute/cows.git
  Fetching https://github.com/modswift/CApache.git
  Fetching https://github.com/swift-server/http
  Cloning https://github.com/modswift/CApache.git
  Resolving https://github.com/modswift/CApache.git at 1.0.0
  Cloning https://github.com/modswift/http.git
  Resolving https://github.com/modswift/http.git at implementation/mod_swift
  Cloning https://github.com/swift-server/http
  Resolving https://github.com/swift-server/http at 0.1.0
  Cloning https://github.com/AlwaysRightInstitute/cows.git
  Resolving https://github.com/AlwaysRightInstitute/cows.git at 1.0.2
  Compile Swift Module 'HTTP' (10 sources)
  Compile Swift Module 'cows' (3 sources)
  Compile Swift Module 'sssfun' (2 sources)

Then start Apache:

swift apache serve
  Note: DocRoot /usr/local/var/www (create a 'public' dir to use an own one)
  Starting Apache on port 8042/8442: // yes, a bug ;-)
  GET /hello 200 295 - 1ms
  GET /hello 200 654 - 0ms
  GET /hello/ 200 870 - 2ms

And hit is using: http://localhost:8042/hello. Or hit it using HTTPS (self signed certificate): https://localhost:8442/hello. If you are using Chrome and open the Developer Tools, you’ll notice it is talking HTTP/2 to the server (Protocol: h2).

Summary

Looks like Server Side Swift is getting somewhere. To get going with simple endpoints there is no need anymore to download big application server packages with tons of dependencies. You can simply drop in the HTTP API package, that’s it. And if you have more complex needs: mod_swift and ApacheExpress 😬

Contact

Hey, we love feedback! Twitter, any of those: @helje5, @ar_institute, @mod_swift, @ApacheExpress3, @noze_io. Email: wrong@alwaysrightinstitute.com.

Written on October 28, 2017