Introduction
FlyingFox is a lightweight HTTP server built using Swift Concurrency. The server uses non blocking BSD sockets, handling each connection in a concurrent child Task. When a socket is blocked with no data, tasks are suspended using the shared AsyncSocketPool
.
Installation
FlyingFox can be installed by using Swift Package Manager.
Note: FlyingFox requires Swift 5.5 on Xcode 13.2+ or Linux to build. It runs on iOS 13+, tvOS 13+ or macOS 10.15+.
To install using Swift Package Manager, add this to the dependencies:
section in your Package.swift file:
.package(url: "https://github.com/swhitty/FlyingFox.git", .upToNextMajor(from: "0.2.0")),
Usage
Start the server by providing a port number:
import FlyingFox
let server = HTTPServer(port: 8080)
try await server.start()
The server runs within the the current task. To stop the server, cancel the task:
let task = Task { try await server.start() }
task.cancel()
Handlers
Handlers can be added to the server for a corresponding route:
await server.appendHandler(for: "/hello") { request in
try await Task.sleep(nanoseconds: 1_000_000_000)
return HTTPResponse(statusCode: .ok,
body: "Hello World!".data(using: .utf8)!)
}
Incoming requests are routed to the first handler with a matching route.
Any unmatched requests receive HTTP 404
.
FileHTTPHandler
Requests can be routed to static files via FileHTTPHandler
:
await server.appendHandler(for: "GET /mock", handler: .file(named: "mock.json"))
FileHTTPHandler
will return HTTP 404
if the file does not exist.
ProxyHTTPHandler
Requests can be proxied via a base URL:
await server.appendHandler(for: "GET *", handler: .proxy(via: "https://pie.dev"))
// GET /get?fish=chips ----> GET https://pie.dev/get?fish=chips
RedirectHTTPHandler
Requests can be redirected to a URL:
await server.appendHandler(for: "GET /fish/*", handler: .redirect(to: "https://pie.dev/get"))
// GET /fish/chips ---> HTTP 301
// Location: https://pie.dev/get
Wildcards
Routes can include wildcards which can be pattern matched against paths:
let HTTPRoute("/hello/*/world")
route ~= "/hello/fish/world" // true
route ~= "GET /hello/fish/world" // true
route ~= "POST hello/dog/world/" // true
route ~= "/hello/world" // false
By default routes accept all HTTP methods, but specific methods can be supplied:
let HTTPRoute("GET /hello/world")
route ~= "GET /hello/world" // true
route ~= "PUT /hello/world" // false
AsyncSocket / PollingSocketPool
Internally, FlyingFox uses standard BSD sockets configured with the flag O_NONBLOCK
. When data is unavailable for a socket (EWOULDBLOCK
) the task is suspended using the current AsyncSocketPool
until data is available:
protocol AsyncSocketPool {
func suspend(untilReady socket: Socket) async throws
}
PollingSocketPool
is currently the only pool available. It uses a continuous loop of poll(2)
/ Task.yield()
to check all sockets awaiting data at a supplied interval. All sockets share the same pool.
Command line app
An example command line app FlyingFoxCLI is available here.
Credits
FlyingFox is primarily the work of Simon Whitty.