{"id":13871909,"url":"https://github.com/swhitty/FlyingFox","last_synced_at":"2025-07-16T01:31:59.250Z","repository":{"id":42269953,"uuid":"459033780","full_name":"swhitty/FlyingFox","owner":"swhitty","description":"Lightweight, HTTP server written in Swift using async/await.","archived":false,"fork":false,"pushed_at":"2025-07-05T23:35:03.000Z","size":1372,"stargazers_count":570,"open_issues_count":10,"forks_count":52,"subscribers_count":16,"default_branch":"main","last_synced_at":"2025-07-14T20:39:00.441Z","etag":null,"topics":["async","async-await","asyncio","http","networking","nio","server","server-side-swift","swift","web","web-server"],"latest_commit_sha":null,"homepage":"","language":"Swift","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/swhitty.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null}},"created_at":"2022-02-14T05:54:30.000Z","updated_at":"2025-07-09T10:28:52.000Z","dependencies_parsed_at":"2023-10-26T08:32:12.092Z","dependency_job_id":"62de0bbb-7e82-47e1-9c53-ff8bff5d1d79","html_url":"https://github.com/swhitty/FlyingFox","commit_stats":{"total_commits":472,"total_committers":7,"mean_commits":67.42857142857143,"dds":0.06779661016949157,"last_synced_commit":"54ef8ac7798b47fad93d3b0f4630c36499b163c4"},"previous_names":[],"tags_count":30,"template":false,"template_full_name":null,"purl":"pkg:github/swhitty/FlyingFox","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/swhitty%2FFlyingFox","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/swhitty%2FFlyingFox/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/swhitty%2FFlyingFox/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/swhitty%2FFlyingFox/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/swhitty","download_url":"https://codeload.github.com/swhitty/FlyingFox/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/swhitty%2FFlyingFox/sbom","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":265371763,"owners_count":23754622,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["async","async-await","asyncio","http","networking","nio","server","server-side-swift","swift","web","web-server"],"created_at":"2024-08-05T23:00:30.083Z","updated_at":"2025-07-16T01:31:59.232Z","avatar_url":"https://github.com/swhitty.png","language":"Swift","readme":"[![Build](https://github.com/swhitty/FlyingFox/actions/workflows/build.yml/badge.svg)](https://github.com/swhitty/FlyingFox/actions/workflows/build.yml)\n[![Codecov](https://codecov.io/gh/swhitty/FlyingFox/graphs/badge.svg)](https://codecov.io/gh/swhitty/FlyingFox)\n[![Platforms](https://img.shields.io/endpoint?url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2Fswhitty%2FFlyingFox%2Fbadge%3Ftype%3Dplatforms)](https://swiftpackageindex.com/swhitty/FlyingFox)\n[![Swift 6.0](https://img.shields.io/endpoint?url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2Fswhitty%2FFlyingFox%2Fbadge%3Ftype%3Dswift-versions)](https://swiftpackageindex.com/swhitty/FlyingFox)\n\n# Introduction\n\n**FlyingFox** is a lightweight HTTP server built using [Swift Concurrency](https://docs.swift.org/swift-book/LanguageGuide/Concurrency.html). The server uses non blocking BSD sockets, handling each connection in a concurrent child [Task](https://developer.apple.com/documentation/swift/task). When a socket is blocked with no data, tasks are suspended using the shared [`AsyncSocketPool`](#pollingsocketpool).\n\n- [Installation](#installation)\n- [Usage](#usage)\n- [Handlers](#handlers)\n- [Routes](#routes)\n    - [Route Parameters](#route-parameters)\n- [Macros](#macros)\n- [WebSockets](#websockets)\n- [FlyingSocks](#flyingsocks)\n    - [Socket](#socket)\n    - [AsyncSocket](#asyncsocket)\n        - [AsyncSocketPool](#asyncsocketpool)\n        - [SocketPool](#socketpool)\n    - [SocketAddress](#socketaddress)\n- [Command Line App](#command-line-app)\n- [Credits](#credits)\n\n# Installation\n\nFlyingFox can be installed by using Swift Package Manager.\n\n**Note:** FlyingFox requires Swift 5.10 on Xcode 15.4+. It runs on iOS 13+, tvOS 13+, watchOS 8+, macOS 10.15+ and Linux. Android and Windows 10 support is experimental.\n\nTo install using Swift Package Manager, add this to the `dependencies:` section in your Package.swift file:\n\n```swift\n.package(url: \"https://github.com/swhitty/FlyingFox.git\", .upToNextMajor(from: \"0.24.1\"))\n```\n\n# Usage\n\nStart the server by providing a port number:\n\n```swift\nimport FlyingFox\n\nlet server = HTTPServer(port: 80)\ntry await server.run()\n```\n\nThe server runs within the the current task. To stop the server, cancel the task terminating all connections immediatley:\n\n```swift\nlet task = Task { try await server.run() }\ntask.cancel()\n```\n\nGracefully shutdown the server after all existing requests complete, otherwise forcefully closing after a timeout:\n\n```swift\nawait server.stop(timeout: 3)\n```\n\nWait until the server is listening and ready for connections:\n\n```swift\ntry await server.waitUntilListening()\n```\n\nRetrieve the current listening address:\n\n```swift\nawait server.listeningAddress\n```\n\n\u003e Note: iOS will hangup the listening socket when an app is suspended in the background. Once the app returns to the foreground, `HTTPServer.run()` detects this, throwing `SocketError.disconnected`. The server must then be started once more.\n\n## Handlers\n\nHandlers can be added to the server by implementing `HTTPHandler`:\n\n```swift\nprotocol HTTPHandler {\n  func handleRequest(_ request: HTTPRequest) async throws -\u003e HTTPResponse\n}\n```\n\nRoutes can be added to the server delegating requests to a handler:\n\n```swift\nawait server.appendRoute(\"/hello\", to: handler)\n```\n\nThey can also be added to closures:\n\n```swift\nawait server.appendRoute(\"/hello\") { request in\n  try await Task.sleep(nanoseconds: 1_000_000_000)\n  return HTTPResponse(statusCode: .ok)\n}\n```\n\nIncoming requests are routed to the handler of the first matching route.\n\nHandlers can throw `HTTPUnhandledError` if after inspecting the request, they cannot handle it.  The next matching route is then used.\n\nRequests that do not match any handled route receive `HTTP 404`.\n\n### FileHTTPHandler\n\nRequests can be routed to static files with `FileHTTPHandler`:\n\n```swift\nawait server.appendRoute(\"GET /mock\", to: .file(named: \"mock.json\"))\n```\n\n`FileHTTPHandler` will return `HTTP 404` if the file does not exist.\n\n\n[Range requests](https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/Range_requests) are supported, responding with `HTTP 206 Partial Content` allowing for efficient streaming of media content:\n\n```swift\nawait server.appendRoute(\"GET,HEAD /jaws\", to: .file(named: \"jaws.m4v\"))\n```\n\n### DirectoryHTTPHandler\n\nRequests can be routed to static files within a directory with `DirectoryHTTPHandler`:\n\n```swift\nawait server.appendRoute(\"GET /mock/*\", to: .directory(subPath: \"Stubs\", serverPath: \"mock\"))\n// GET /mock/fish/index.html  ----\u003e  Stubs/fish/index.html\n```\n\n`DirectoryHTTPHandler` will return `HTTP 404` if a file does not exist.\n\n### ProxyHTTPHandler\n\nRequests can be proxied via a base URL:\n\n```swift\nawait server.appendRoute(\"GET *\", to: .proxy(via: \"https://pie.dev\"))\n// GET /get?fish=chips  ----\u003e  GET https://pie.dev/get?fish=chips\n```\n\n### RedirectHTTPHandler\n\nRequests can be redirected to a URL:\n\n```swift\nawait server.appendRoute(\"GET /fish/*\", to: .redirect(to: \"https://pie.dev/get\"))\n// GET /fish/chips  ---\u003e  HTTP 301\n//                        Location: https://pie.dev/get\n```\n\n### WebSocketHTTPHandler\n\nRequests can be routed to a websocket by providing a `WSMessageHandler` where a pair of `AsyncStream\u003cWSMessage\u003e` are exchanged:\n```swift\nawait server.appendRoute(\"GET /socket\", to: .webSocket(EchoWSMessageHandler()))\n\nprotocol WSMessageHandler {\n  func makeMessages(for client: AsyncStream\u003cWSMessage\u003e) async throws -\u003e AsyncStream\u003cWSMessage\u003e\n}\n\nenum WSMessage {\n  case text(String)\n  case data(Data)\n  case close(WSCloseCode)\n}\n```\n\nRaw WebSocket frames can also be [provided](#websockets).\n\n### RoutedHTTPHandler\n\nMultiple handlers can be grouped with requests and matched against `HTTPRoute` using `RoutedHTTPHandler`.\n\n```swift\nvar routes = RoutedHTTPHandler()\nroutes.appendRoute(\"GET /fish/chips\", to: .file(named: \"chips.json\"))\nroutes.appendRoute(\"GET /fish/mushy_peas\", to: .file(named: \"mushy_peas.json\"))\nawait server.appendRoute(for: \"GET /fish/*\", to: routes)\n```\n\n`HTTPUnhandledError` is thrown when it's unable to handle the request with any of its registered handlers.\n\n## Routes\n\n`HTTPRoute` is designed to be [pattern matched](https://docs.swift.org/swift-book/ReferenceManual/Patterns.html#ID426) against `HTTPRequest`, allowing requests to be identified by some or all of its properties. \n\n```swift\nlet route = HTTPRoute(\"/hello/world\")\n\nroute ~= HTTPRequest(method: .GET, path: \"/hello/world\") // true\nroute ~= HTTPRequest(method: .POST, path: \"/hello/world\") // true\nroute ~= HTTPRequest(method: .GET, path: \"/hello/\") // false\n```\n\nRoutes are `ExpressibleByStringLiteral` allowing literals to be automatically converted to `HTTPRoute`:\n\n```swift\nlet route: HTTPRoute = \"/hello/world\"\n```\n\nRoutes can include a specific method to match against:\n\n```swift\nlet route = HTTPRoute(\"GET /hello/world\")\n\nroute ~= HTTPRequest(method: .GET, path: \"/hello/world\") // true\nroute ~= HTTPRequest(method: .POST, path: \"/hello/world\") // false\n```\n\nThey can also use wildcards within the path:\n\n```swift\nlet route = HTTPRoute(\"GET /hello/*/world\")\n\nroute ~= HTTPRequest(method: .GET, path: \"/hello/fish/world\") // true\nroute ~= HTTPRequest(method: .GET, path: \"/hello/dog/world\") // true\nroute ~= HTTPRequest(method: .GET, path: \"/hello/fish/sea\") // false\n```\n\nRoutes can include [parameters](#route-parameters) that match like wildcards allowing handlers to extract the value from the request.\n\n```swift\nlet route = HTTPRoute(\"GET /hello/:beast/world\")\n\nlet beast = request.routeParameters[\"beast\"]\n```\n\nTrailing wildcards match all trailing path components:\n\n```swift\nlet route = HTTPRoute(\"/hello/*\")\n\nroute ~= HTTPRequest(method: .GET, path: \"/hello/fish/world\") // true\nroute ~= HTTPRequest(method: .GET, path: \"/hello/dog/world\") // true\nroute ~= HTTPRequest(method: .POST, path: \"/hello/fish/deep/blue/sea\") // true\n```\n\nSpecific query items can be matched:\n\n```swift\nlet route = HTTPRoute(\"/hello?time=morning\")\n\nroute ~= HTTPRequest(method: .GET, path: \"/hello?time=morning\") // true\nroute ~= HTTPRequest(method: .GET, path: \"/hello?count=one\u0026time=morning\") // true\nroute ~= HTTPRequest(method: .GET, path: \"/hello\") // false\nroute ~= HTTPRequest(method: .GET, path: \"/hello?time=afternoon\") // false\n```\n\nQuery item values can include wildcards:\n\n```swift\nlet route = HTTPRoute(\"/hello?time=*\")\n\nroute ~= HTTPRequest(method: .GET, path: \"/hello?time=morning\") // true\nroute ~= HTTPRequest(method: .GET, path: \"/hello?time=afternoon\") // true\nroute ~= HTTPRequest(method: .GET, path: \"/hello\") // false\n```\n\nHTTP headers can be matched:\n\n```swift\nlet route = HTTPRoute(\"*\", headers: [.contentType: \"application/json\"])\n\nroute ~= HTTPRequest(headers: [.contentType: \"application/json\"]) // true\nroute ~= HTTPRequest(headers: [.contentType: \"application/xml\"]) // false\n```\n\nHeader values can be wildcards:\n\n```swift\nlet route = HTTPRoute(\"*\", headers: [.authorization: \"*\"])\n\nroute ~= HTTPRequest(headers: [.authorization: \"abc\"]) // true\nroute ~= HTTPRequest(headers: [.authorization: \"xyz\"]) // true\nroute ~= HTTPRequest(headers: [:]) // false\n```\n\nBody patterns can be created to match the request body data:\n\n```swift\npublic protocol HTTPBodyPattern: Sendable {\n  func evaluate(_ body: Data) -\u003e Bool\n}\n```\n\nDarwin platforms can pattern match a JSON body with an [`NSPredicate`](https://developer.apple.com/documentation/foundation/nspredicate):\n\n```swift\nlet route = HTTPRoute(\"POST *\", body: .json(where: \"food == 'fish'\"))\n```\n```json\n{\"side\": \"chips\", \"food\": \"fish\"}\n```\n\n## Route Parameters\n\nRoutes can include named parameters within a path or query item using the `:` prefix. Any string supplied to this parameter will match the route, handlers can access the value of the string using `request.routeParameters`.\n\n```swift\nhandler.appendRoute(\"GET /creature/:name?type=:beast\") { request in\n  let name = request.routeParameters[\"name\"]\n  let beast = request.routeParameters[\"beast\"]\n  return HTTPResponse(statusCode: .ok)\n}\n```\n\nRoute parameters can be automatically extracted and mapped to closure parameters of handlers.\n\n```swift\nenum Beast: String, HTTPRouteParameterValue {\n  case fish\n  case dog\n}\n\nhandler.appendRoute(\"GET /creature/:name?type=:beast\") { (name: String, beast: Beast) -\u003e HTTPResponse in\n  return HTTPResponse(statusCode: .ok)\n}\n```\n\nThe request can be optionally included.\n\n```swift\nhandler.appendRoute(\"GET /creature/:name?type=:beast\") { (request: HTTPRequest, name: String, beast: Beast) -\u003e HTTPResponse in\n  return HTTPResponse(statusCode: .ok)\n}\n```\n\n`String`, `Int`, `Double`, `Bool` and any type that conforms to `HTTPRouteParameterValue` can be extracted.\n\n## WebSockets\n`HTTPResponse` can switch the connection to the [WebSocket](https://datatracker.ietf.org/doc/html/rfc6455) protocol by provding a `WSHandler` within the response payload.\n\n```swift\nprotocol WSHandler {\n  func makeFrames(for client: AsyncThrowingStream\u003cWSFrame, Error\u003e) async throws -\u003e AsyncStream\u003cWSFrame\u003e\n}\n```\n\n`WSHandler` facilitates the exchange of a pair `AsyncStream\u003cWSFrame\u003e` containing the raw websocket frames sent over the connection. While powerful, it is more convenient to exchange streams of messages via [`WebSocketHTTPHandler`](#websockethttphandler).\n\n## Macros\n\nThe repo [`FlyingFoxMacros`](https://github.com/swhitty/FlyingFoxMacros) contains macros that can be annotated with `HTTPRoute` to automatically syntesise a `HTTPHandler`.\n\n```swift\nimport FlyingFox\nimport FlyingFoxMacros\n\n@HTTPHandler\nstruct MyHandler {\n\n  @HTTPRoute(\"/ping\")\n  func ping() { }\n\n  @HTTPRoute(\"/pong\")\n  func getPong(_ request: HTTPRequest) -\u003e HTTPResponse {\n    HTTPResponse(statusCode: .accepted)\n  }\n\n  @JSONRoute(\"POST /account\")\n  func createAccount(body: AccountRequest) -\u003e AccountResponse {\n    AccountResponse(id: UUID(), balance: body.balance)\n  }\n}\n\nlet server = HTTPServer(port: 80, handler: MyHandler())\ntry await server.run()\n```\n\nThe annotations are implemented via [SE-0389 Attached Macros](https://github.com/apple/swift-evolution/blob/main/proposals/0389-attached-macros.md).\n\nRead more [here](https://github.com/swhitty/FlyingFoxMacros).\n\n# FlyingSocks\n\nInternally, FlyingFox uses a thin wrapper around standard BSD sockets. The `FlyingSocks` module provides a cross platform async interface to these sockets;\n\n```swift\nimport FlyingSocks\n\nlet socket = try await AsyncSocket.connected(to: .inet(ip4: \"192.168.0.100\", port: 80))\ntry await socket.write(Data([0x01, 0x02, 0x03]))\ntry socket.close()\n```\n\n## Socket\n\n`Socket` wraps a file descriptor and provides a Swift interface to common operations, throwing `SocketError` instead of returning error codes.\n\n```swift\npublic enum SocketError: LocalizedError {\n  case blocked\n  case disconnected\n  case unsupportedAddress\n  case failed(type: String, errno: Int32, message: String)\n  case timeout(message: String)\n}\n```\n\nWhen data is unavailable for a socket and the `EWOULDBLOCK` errno is returned, then `SocketError.blocked` is thrown.\n\n## AsyncSocket\n\n`AsyncSocket` simply wraps a `Socket` and provides an async interface.  All async sockets are configured with the flag `O_NONBLOCK`, catching `SocketError.blocked` and then suspending the current task using an `AsyncSocketPool`.  When data becomes available the task is resumed and `AsyncSocket` will retry the operation.\n\n### AsyncSocketPool\n\n```swift\nprotocol AsyncSocketPool {\n  func prepare() async throws\n  func run() async throws\n\n  // Suspends current task until a socket is ready to read and/or write\n  func suspendSocket(_ socket: Socket, untilReadyFor events: Socket.Events) async throws\n}\n```\n\n### SocketPool\n\n[`SocketPool\u003cQueue\u003e`](https://github.com/swhitty/FlyingFox/blob/main/FlyingSocks/Sources/SocketPool.swift) is the default pool used within `HTTPServer`. It suspends and resume sockets using its generic `EventQueue` depending on the platform. Abstracting [`kqueue(2)`](https://www.freebsd.org/cgi/man.cgi?kqueue) on Darwin platforms and [`epoll(7)`](https://man7.org/linux/man-pages/man7/epoll.7.html) on Linux, the pool uses kernel events without the need to continuosly poll the waiting file descriptors.\n\nWindows uses a queue backed by a continuous loop of [`poll(2)`](https://www.freebsd.org/cgi/man.cgi?poll) / [`Task.yield()`](https://developer.apple.com/documentation/swift/task/3814840-yield) to check all sockets awaiting data at a supplied interval. \n\n## SocketAddress\n\nThe `sockaddr` cluster of structures are grouped via conformance to `SocketAddress`\n- `sockaddr_in`\n- `sockaddr_in6`\n- `sockaddr_un`\n\nThis allows `HTTPServer` to be started with any of these configured addresses:\n\n```swift\n// only listens on localhost 8080\nlet server = HTTPServer(address: .loopback(port: 8080))\n```\n\nIt can also be used with [UNIX-domain](https://www.freebsd.org/cgi/man.cgi?query=unix) addresses, allowing private IPC over a socket:\n\n```swift\n// only listens on Unix socket \"Ants\"\nlet server = HTTPServer(address: .unix(path: \"Ants\"))\n```\n\nYou can then [netcat](https://www.freebsd.org/cgi/man.cgi?query=nc) to the socket:\n```\n% nc -U Ants\n```\n\n# Command line app\n\nAn example command line app FlyingFoxCLI is available [here](https://github.com/swhitty/FlyingFoxCLI).\n\n# Credits\n\nFlyingFox is primarily the work of [Simon Whitty](https://github.com/swhitty).\n\n([Full list of contributors](https://github.com/swhitty/FlyingFox/graphs/contributors))\n","funding_links":[],"categories":["Swift"],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fswhitty%2FFlyingFox","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fswhitty%2FFlyingFox","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fswhitty%2FFlyingFox/lists"}