objecthub/swift-nanohttp
NanoHTTP is a small, extensible HTTP/1.1 server framework for macOS, iOS and tvOS. It runs a web server inside your
A first server
import Foundation
import NanoHTTP
let server = NanoHTTPServer()
server.get["/hello/:name"] = { request in
.ok(.text("Hello, \(request.params["name"] ?? "stranger")!"))
}
server.post["/echo"] = { request in
.ok(.data(Data(request.body), contentType: request.header("content-type")))
}
server.get["/time"] = { _ in
.ok(.json(["now": ISO8601DateFormatter().string(from: Date())]))
}
try server.start(8080)
print("Serving at http://localhost:\(try server.port())")
RunLoop.main.run()curl http://localhost:8080/hello/AdaA handler is a function from a request to a response. Paths can have variables (/users/:id), routes can be grouped and mounted below a prefix, and server["/path"] answers every method.
What it can do
- Routing with
:variables, wildcards and path variables, per-method registration, sub-routers. - Requests: headers, query, route variables, URL-encoded forms, multipart uploads, bodies of up to 64 MiB
(configurable).
- Responses: text, HTML, JSON (from any
Encodable), bytes, redirects, cookies, streamed bodies (in chunks, so that
connections are reused), any status code.
- HTTP/1.1 as clients expect it: persistent connections, chunked request bodies,
Expect: 100-continue,HEAD,
405 with Allow, OPTIONS, a Date header.
- Files:
NanoFiles.share(file:),NanoFiles.share(directory:)andNanoFiles.browse(directory:), which do not serve anything from outside
the directory, with ETag, Last-Modified, 304 Not Modified and byte ranges (206).
- WebSockets (RFC 6455): fragmented messages, ping/pong, close codes, subprotocols, a check of the page's origin, a hub
that sends to many sessions, thread-safe sending, size limits.
- NanoHTML, a second library for small pages: elements are Swift values (
Html { Body { H1("Hi") } }), text and attributes are
escaped, and building pages needs no locks.
- Errors: a handler can
throw NanoHTTPError.notFound("No such item"), which becomes the response; other
errors become a 500 without details.
- Middleware that wraps requests (check, change, log, catch errors), and subclassing for everything else:
how requests are dispatched and how connections are handled.
- Logging with levels: what the server reports goes to a logger (the unified log by default, the console or your own),
not to print.
- Limits for lines, headers and bodies, timeouts for idle and stalled connections, a limit for the number of
connections, and answers (400, 408, 413, 414, 431, 503) for requests that exceed them.
By default a thread serves each connection, so handlers of different connections run at the same time. Or the server runs on one thread (server.configuration.threading = .singleThread): requests and WebSocket messages are handled one at a time and handlers need no locks.
Pages
The second library of the package, NanoHTML (documentation), builds small pages (status pages, forms, the user interface of a tool) from Swift values. Text and attribute values are escaped, so a page cannot be broken by what a request contains:
import NanoHTTP
import NanoHTML
let server = NanoHTTPServer()
server["/hello/:name"] = htmlHandler { request in
Html {
Head { Title("Hello") }
Body {
H1("Hello, \(request.params["name"] ?? "stranger")!") // escaped
Ul {
for header in request.availableHeaders.sorted() {
Li("\(header): \(request.header(header) ?? "")")
}
}
A("Home").href("/").class("button")
}
}
}Serving files
let server = NanoHTTPServer()
let directory = NSHomeDirectory() + "/Public"
server["/files/::path"] = NanoFiles.share(directory: directory) // GET /files/photos/cat.jpg
server["/browse/::path"] = NanoFiles.browse(directory: directory) // a listing of the directoryRequests with .., NUL characters or symbolic links that lead out of the directory are refused with 403.
WebSockets
let server = NanoHTTPServer()
server["/chat"] = NanoWebSocket.handler(
text: { session, message in session.writeText("You said: \(message)") },
connected: { session in session.writeText("Welcome!") })A NanoWebSocketHub keeps the sessions that belong together and sends a message to all of them, from any thread and without locks of your own:
let server = NanoHTTPServer()
let chat = NanoWebSocketHub()
server["/chat"] = NanoWebSocket.handler(
text: { session, message in chat.broadcast(text: message, except: session) },
hub: chat)A client that stops reading is dropped after a few seconds, so that it cannot hold up the others.
Security
A server accepts connections from this machine only unless you tell it otherwise. A server that other devices are meant to reach has to say so:
let server = NanoHTTPServer()
server.configuration.listenAddress = .allInterfaces
try server.start(8080)Even a server that listens at the loopback address can be reached by web pages in the user's browser. Two checks, which are off by default, close the usual ways in; switch them on for a server that is used from a browser:
let server = NanoHTTPServer()
server.configuration.hostPolicy = .automatic // refuses other host names (DNS rebinding)
server.configuration.originPolicy = .sameOrigin // refuses requests that change something, and WebSocket
// handshakes, from pages of other sitesNanoMiddleware.cors lets the pages that you trust use the server. There is no TLS, no authentication and no rate limiting: do not expose a server to the internet without a reverse proxy that provides them. The documentation article Security and Limits lists what to decide, with code. To report a vulnerability, see SECURITY.md.
Performance
Measured against 1.0.2 with Benchmarks/run.py, over the loopback interface on one Mac. Every figure is the median of the ratios of runs made back to back (the same machine measures 15% apart in two runs minutes apart, so only such ratios mean something):
| Workload | Requests per second | CPU per request | |---|---|---| | one client, persistent connection | 1.3x | 0.5x | | 16 clients, persistent connections | about 5x | 0.1x | | 100 KB file served by NanoFiles.share(file:), 4 clients | 7x to 11x | 0.4x to 0.8x | | WebSocket echo, 1 client / 16 clients | about 1.1x / 1.1x | 0.8x / 0.65x | | a new connection for every request, 1 client / 16 clients | 1.0x / 1.0x | 0.7x / 0.5x | | 100 KB and 10 MB responses, 1 MB uploads | 1.0x | 0.8x, 1.0x, 1.0x |
Where two numbers are given, they are the results of two full runs (of 2.0 and of 2.1): the throughput of the file scenario varies by a third from run to run, so only the paired ratios mean something. Version 2.1 is as fast as 2.0 (paired runs against the 2.0.0 tag: 0.99x to 1.03x in both modes).
The one-thread mode (threading = .singleThread) gains about the same against the one-thread mode of 1.0.2: 1.3x with one persistent connection, the same speed for a new connection per request, and 0.5x to 0.9x of the CPU per request. The one-thread mode of 1.0.2 did not serve several persistent connections at the same time; now 16 of them get 4.8x the requests per second. What the scenarios do is described in Benchmarks/README.md, together with the way to repeat the measurement.
Installation
Add the package to your Package.swift:
.package(url: "https://github.com/objecthub/swift-nanohttp.git", from: "2.1.2"),and the product NanoHTTP (and NanoHTML, if you build pages with it) to the dependencies of your target. In Xcode, use File > Add Package Dependencies.
Documentation
The API reference and the articles are also available as online documentation (and for the companion library, NanoHTML), generated with Swift-DocC.
The API is documented with DocC: a catalog for each library, with articles (getting started, routing, requests and responses, files, WebSockets, middleware, threading, logging, security, testing, iOS, building pages, examples) and documentation comments for every public symbol. Build it in Xcode with Product > Build Documentation, or from the command line, keeping the archive to open in Xcode:
python3 Scripts/check_docs.py --output docs # docs/NanoHTTP.doccarchive and docs/NanoHTML.doccarchivePublishing on GitHub Pages
The documentation site is served from the root of branch gh-pages. Script Scripts/generate-docs.sh generates the site for both libraries with the Swift-DocC plugin and copies it into a git worktree of that branch in .build/gh-pages. Package.swift only adds the plugin to the package's dependencies if environment variable NANOHTTP_GENERATE_DOCS is set (which the script does), so packages that depend on NanoHTTP do not get to see it. The script neither commits nor pushes anything:
Scripts/generate-docs.sh
cd .build/gh-pages
git add -A
git commit -m "Update the documentation site"
git push -u origin gh-pagesSince the site documents the current state of the working copy, it should be generated before tagging a release.
The examples in the documentation are compiled against the library by Scripts/check_doc_examples.py, so they stay correct. The CHANGELOG lists what changed in every version, and ARCHITECTURE.md describes how the package is put together, for people who change it.
Examples
Six small programs show how the pieces fit together. Each is a complete program of a page or two, and python3 Scripts/check_examples.py runs them all against real requests:
| Run | What it shows | |---|---| | swift run ExampleHello | routes, route variables, query parameters, a page for unknown paths | | swift run ExampleTodoAPI | a JSON API: Codable models, status codes, errors as JSON, middleware | | swift run ExampleFileServer --directory ~/Downloads | NanoFiles.share and browse, the Host and Origin policies | | swift run ExampleChat | WebSockets and a NanoWebSocketHub, a page built with NanoHTML | | swift run ExampleStatus | a server with one thread: shared state without locks, a live page over a WebSocket | | swift run ExampleUpload | a file upload form, limits, cookies, post/redirect/get |
They listen at the loopback address (--network makes them reachable from other devices) on port 8080 (--port), and Ctrl-C stops them.
Demo servers
The kitchen sink is a demo server, which runs as a command-line tool on macOS and as a SwiftUI app on iOS. It shows many features, and serves a folder on port 9080: the Downloads folder in the command-line tool. The tool is reachable from this machine only; start it with --network to let other devices connect, on networks you trust:
swift run DemoServer # http://localhost:9080
swift run DemoServer --network # reachable from the network
swift run DemoServer --single-thread # everything on one thread
swift run DemoServer --directory ~/Public --port 9000 # another folder and portThe iOS app listens on all network interfaces, because its purpose is to be reached from other devices.
Requirements
- macOS 12, iOS 15 or tvOS 15, or later
- Xcode 15 and Swift 5.9 or later
- Swift Package Manager
Development
swift test # the tests (see Tests/README.md)
swift test --sanitize=thread # also with --sanitize=address
python3 Scripts/check_xcodeproj.py # the Xcode project lists every Swift file and matches Package.swift
python3 Scripts/check_doc_examples.py # the examples of the documentation compile
python3 Scripts/check_docs.py # the DocC documentation builds without warnings
python3 Scripts/check_examples.py # the programs in Examples/ run and stop (also: --sanitize=thread)
python3 Scripts/check_demo.py # the demo server, in both threading modes, against curl and a WebSocket client
python3 Benchmarks/run.py --baseline 1.0.2 # throughput, latency and CPU use against a release (see Benchmarks/README.md)See CONTRIBUTING.md for how to contribute.
License
NanoHTTP is available under the BSD 3-Clause license, see LICENSE.
Copyright
Author: Matthias Zenger (<matthias@objecthub.com>) Copyright © 2024-2026 Matthias Zenger. All rights reserved.
Package Metadata
Repository: objecthub/swift-nanohttp
Default branch: main
README: README.md