Fork notice. This is a self-maintained fork of httpswift/swifter that drops the BSD-socket I/O layer and rebuilds it on Apple's Network.framework (
NWListener/NWConnection). Headline additions: async handlers, async middleware, custom 404, and HTTPS via TLSConfig (file path or in-memory bytes, with optional TLS version pinning). The upstreamHttpRequest/HttpResponse/ routing / WebSocket frame parsing are preserved so existing handlers compile unchanged.Upstream sync (re-base or PR back) is not a goal; Linux is unsupported.
Tiny HTTP server engine written in Swift, embeddable in apps and extensions. Originally by Damian Kołakowski and the swifter contributors.
- Swift 5.9+
- macOS 12+, iOS 15+, tvOS 15+, watchOS 8+
- No external dependencies —
Network.frameworkandSecurityare system frameworks.
import Swifter
let server = HttpServer()
server["/hello"] = { req in
.ok(.htmlBody("You asked for \(req.path)"))
}
// start() is async now
try await server.start(8080)
print("Listening on port \(try server.port())")let server = HttpServer()
// sync handler — unchanged API
server.GET["/sync"] = { _ in .ok(.text("hi")) }
// async handler
server.GET.setAsync("/slow") { _ in
try await Task.sleep(nanoseconds: 100_000_000)
return .ok(.text("done"))
}
try await server.start(8080)Async handlers run concurrently per-request; five 100ms handlers complete
in ~110ms, not 500ms (see HttpServerIOEndToEndTests.testAsyncHandlersRunConcurrently).
Each layer registered with server.use { ... } runs before route
dispatch, in registration order. Return nil to pass through, or an
HttpResponse to short-circuit. Throwing becomes a 500.
let server = HttpServer()
server.use { req in
if req.headers["x-auth"] == nil {
return .unauthorized(.text("missing x-auth"))
}
return nil // pass through
}
server.GET["/secret"] = { _ in .ok(.text("you got it")) }
try await server.start(8080)The legacy sync server.middleware = [...] array from upstream is still
honored and runs before the async chain.
server.notFoundAsyncHandler = { req in
.notFound(.text("no route: \(req.path)"))
}
// sync variant is still there for legacy callers
server.notFoundHandler = { _ in .notFound(.text("nope")) }When both are set, the async handler wins.
TLSConfig accepts a PKCS#12 identity from a file path or from
raw bytes (the bytes path is convenient when the P12 lives in
Keychain or another in-memory source). Minimum/maximum TLS version
default to "≥1.2 with no upper bound" but can be pinned.
import Swifter
// from a file
let tls = try TLSConfig.p12(
path: "/path/to/server.p12",
password: "your-p12-password"
)
// from bytes, locked to TLS 1.3 only
let bytes: Data = loadFromKeychain()
let strictTLS = try TLSConfig.p12(
data: bytes,
password: "your-p12-password",
minVersion: .v1_3,
maxVersion: .v1_3
)
let server = HttpServer()
server.GET["/ping"] = { _ in .ok(.text("pong over tls")) }
try await server.start(8443, tls: tls)Only TLSv1.2 and TLSv1.3 are exposed — Apple deprecated TLSv1.0/1.1 in macOS 12 / iOS 15.
Generating a self-signed P12 for development:
openssl req -x509 -newkey rsa:2048 \
-keyout localhost.key -out localhost.crt \
-days 3650 -nodes -subj "/CN=localhost" \
-addext "subjectAltName=DNS:localhost,IP:127.0.0.1"
openssl pkcs12 -export -out localhost.p12 \
-inkey localhost.key -in localhost.crt \
-name "swifter-test" -password pass:swiftertest \
-keypbe PBE-SHA1-3DES -certpbe PBE-SHA1-3DESlet server = HttpServer()
server["/echo"] = websocket(
text: { session, text in session.writeText(text) },
binary: { session, data in session.writeBinary(data) },
connected: { _ in print("connected") },
disconnected: { _ in print("disconnected") }
)
try await server.start(8080)WebSocketSession's writeText / writeBinary / writeCloseFrame are
sync on purpose — JS-bridge callers see the same API. Internally they
enqueue onto the transport's sendNonBlocking path. session.close()
sends a close frame and cancels the transport.
| Area | Upstream | This fork |
|---|---|---|
| Socket layer | BSD socket(2) + accept loop |
NWListener + NWConnection |
start() |
sync try server.start(8080) |
try await server.start(8080, forceIPv4: false, tls: nil) |
| Handlers | (HttpRequest) -> HttpResponse |
sync or (HttpRequest) async throws -> HttpResponse |
| Middleware | sync only ([Request -> Response?]) |
sync chain + async chain via server.use { ... } |
| Custom 404 | notFoundHandler (sync) |
notFoundHandler (sync) + notFoundAsyncHandler (async) |
| HTTPS | not supported | TLSConfig.p12(path:...) or TLSConfig.p12(data:...), with TLS version pinning |
HttpResponse.switchProtocols |
(Socket) -> Void |
@Sendable (HttpTransport) async -> Void |
WebSocketSession.socket |
Socket |
transport: HttpTransport; added WebSocketSession.close() |
| Linux | supported | not supported — Darwin-only |
| iOS background suspend | RUNNINGBOARD 0xdead10cc on socket hold | NWListener participates in system lifecycle |
- Wrap
server.start(...)inawaitand the enclosing function inasync throws. - If you used
HttpServerIODelegate.socketConnectionReceived(_:), rename totransportConnectionReceived(_:)and change the argument type toHttpTransport. HttpResponse.switchProtocolsclosure now takesHttpTransportand must be@Sendable () async -> Void. The built-inwebsocket(...)factory already does this; only matters if you callswitchProtocolsdirectly.
- PEM cert + key loader as an alternative to P12 (will need temporary Keychain assembly on iOS, deferred until a use case shows up)
- Optional client-certificate verification (mTLS)
- Evaluate HTTP/2 over
NWProtocolQUICif a use case shows up
MIT, inherited from upstream. Original copyright Damian Kołakowski; fork modifications retain the same license.