How about catching errors with Hummingbird Middleware?
This article is part of a series.
- Part 1: How can I have Hummingbird serve static files?
- Part 2: What kinda routes can I use with Hummingbird?
- Part 3: How do I test the Hummingbird routes with swift-testing?
- Part 4: How do I get Hummingbird to serve html dynamically?
- Part 5: How do I get Hummingbird to use Mustache templates instead?
- Part 6: How do I use Hummingbird to make a basic CRUD API?
- Part 7: How do I get Hummingbird to work with the OpenAPI plugins?
- Part 8: How do I make a OpenAPI ClownAPI for Hummingbird?
- Part 9: So how exactly does Middleware come into Hummingbird?
- Part 10: This Article
- Part 11: So what even is a Hummingbird RequestContext?
- Part 12: Why would I want a Hummingbird ChildRequestContext?
This article will show four different error responses based on Middleware:
- how to print them to the log
- how to return a static HTML based 404 page
- how to return dynamic HTML in response to any error
- how to choose to send JSON instead.
These can all be seen in the repo along with the examples from the last post.
Order matters in a Middleware stack. All of these examples work by catching the errors relevant to their tasks and returning a Response
. Changing a HTTPError
with a 404 status into a Response
with a 404 status means that middleware which is only looking for unhandled errors will not catch
an already packaged Response
! This is a feature because it let’s the developer progressively filter out errors when requests or the errors meet certain conditions, like in the JSON example.
It’s also good to note what a not-found Middleware can’t do using the trie router, which is to apply to a whole branch of routes selectively. For groups and collections the trie router applies middleware to the found routes only, but there is a way around that discussed at the end.
Log Errors Middleware
It’s not just Responses
that percolate up through middleware, but errors too. From what I can tell most if not all errors thrown by Hummingbird itself will be an HTTPError (docs).
To get a sense of what’s happening LogErrorsMiddleware
will print out the errors it encounters to the Log. In projects with a lot of middleware, moving LogErrorsMiddleware
’s placement around the stack operates much like moving a stethoscope around, triangulating a problem.
import Hummingbird
import Logging
public struct LogErrorsMiddleware<Context: RequestContext>: RouterMiddleware {
var level:Logger.Level
func formatHeaderForLog(headers: HTTPFields) -> String {
var forLog = ""
for header in headers {
forLog.append("\t\(header.name):\(header.value)\n")
}
forLog.append("------")
return forLog
}
public func handle(_ request: Request, context: Context, next: (Request, Context) async throws -> Response) async throws -> Response {
do {
return try await next(request, context)
} catch let error as HTTPError {
let status = error.status
let formattedHeaders = formatHeaderForLog(headers: error.headers)
context.logger.log(level: level, Logger.Message(stringLiteral: "\nResponding\(status) with:\n\(formattedHeaders)"))
throw error
}
}
}
It’s output looks like:
025-09-27T04:10:53-0700 info inthemiddle: hb.request.id=7c21d9a8dea288aed30d15e3536012ea [inthemiddle]
Responding 404 Not Found to /other/ call:
------
There are no headers yet. The curl output shows a handful added at the last moment.
## curl -i "http://localhost:8080/other/"
HTTP/1.1 404 Not Found
Content-Length: 0
Date: Sat, 27 Sep 2025 11:10:52 GMT
Server: inthemiddle
These do not appear to be added by the defaultHummingbirdHeaders method called by the default JSONEncoder, but instead added at the Application layer.
- server name: Application.swift#L132
- content length: Response.swift#L37
- date: Application.swift#L129
I found these lines by searching the Hummingbird repo for the .headerName
as determined by the HTTPField names of the HTTPTypes package.
Return Static Page
The original itch to explore middleware came from wanting to get a better “Not Found” error, just a classic 404 page. Hummingbird formulates an HTTPError
for this situation in the respond
method using the notFoundResponder belonging to the RouterResponder
which the application builds from the Router
passed to it at startup.
This handler performs it’s task classically by using a redirect. It uses the 303 (see other) redirect, telling the browser that trying this page again in the future might get a different response, or maybe not. As such, a client should never cache it.
More on redirects and caching them.
The Static404Middleware
ONLY redirects 404 errors, leaving everything else alone. Since middleware higher up catches requests before the lower ones, but responses after, placing Static404Middleware
below the logger will catch 404’s before the logger has a chance to register them.
router.addMiddleware {
//built in logger.
LogRequestsMiddleware(.info)
//custom middleware for logging errors
//will never log 404's because Static404Middleware
//catches them first.
LogErrorsMiddleware(level: .info)
//Serves static 404 pages
Static404Middleware()
FileMiddleware(searchForIndexHtml: true)
}
Static404Middleware
import Hummingbird
struct Static404Middleware<Context: RequestContext>: RouterMiddleware {
func handle(
_ request: Request,
context: Context,
next: (
Request,
Context
) async throws -> Response
) async throws -> Response {
do {
return try await next(request, context)
}
catch let error as HTTPError {
if error.status == .notFound {
return Response(
status: .seeOther,
headers: [
.location: "/404.html",
]
)
}
throw error
}
}
}
And the static page:
<!DOCTYPE html>
<html>
<head>
<title>Wrong turn.</title>
<meta charset="UTF-8">
</head>
<body>
<H1>Return to the route.</H1>
</body>
</html>
If that’s too uninformative, don’t reach for Javascript’s Document.URL
to pass query args and build a dynamic page that way. Hummingbird’s RIGHT HERE to make that dynamic page itself.
Return Dynamic Page
This section leans on the previous post on Mustache and code from the auth-fluent example.
The resulting middleware will serve a 404 without any redirection. If the endpoint won’t ever hold valid content that’s a great choice. But not just 404’s, it will also provide a page for ANY error using an ErrorHTML
builder.
ErrorPageMiddleware
import Hummingbird
import Mustache
/// Generate an HTML page for a thrown error
struct ErrorPageMiddleware<Context: RequestContext>: RouterMiddleware {
let mustacheLibrary: MustacheLibrary
func handle(
_ request: Request,
context: Context,
next: (Request, Context) async throws -> Response
) async throws -> Response {
do {
return try await next(request, context)
} catch {
if let error = error as? HTTPError {
return try ErrorHTML(status: error.status, message: error.body ?? "", library: mustacheLibrary).response(from: request, context: context)
} else {
return try ErrorHTML(status: HTTPResponse.Status.internalServerError, message: "\(error)", library: mustacheLibrary).response(from: request, context: context)
}
}
}
}
ErrorHTML
import Hummingbird
import Mustache
struct ErrorInfo {
let title: String
let status: String
let message: String
}
struct ErrorHTML:ResponseGenerator {
let message: String
let status: HTTPResponse.Status
let library: MustacheLibrary
init(status: HTTPResponse.Status, message: String, library:MustacheLibrary) {
self.message = message
self.status = status
self.library = library
//should probably check library for the error template.
}
private var html:String {
if let result = try? render() {
return result
} else {
return "??? oops."
}
}
private func render() throws -> String? {
let pageContent = ErrorInfo(title: "Error Finding Page", status: "\(status)", message: message)
let rendered = library.render(
pageContent,
withTemplate: "error"
)
return rendered
}
func response(from request: Request, context: some RequestContext) throws -> Response {
let buffer = ByteBuffer(string: self.html)
return .init(status: status, headers: [.contentType: "text/html"], body: .init(byteBuffer: buffer))
}
}
The Mustache Templates
error.mustache
calls head.mustache
and foot.mustache
, all in the Templates folder included in Package.swift.
{{>head}}
<body>
<h1>Error</h1>
<h2>{{ status }}</h2>
<p>{{ message }}</p>
</body>
{{>foot}}
<!DOCTYPE html>
<html>
<head>
<title>{{ title }}</title>
<meta charset="UTF-8">
</head>
</html>
Adding the Middleware and Templates
in the buildRouter
function…
let mustacheLibrary = try await MustacheLibrary(directory: Bundle.module.resourcePath!)
router.addMiddleware {
//Mustache based error page.
ErrorPageMiddleware(mustacheLibrary: mustacheLibrary)
//...
}
Return JSON
But what if the app serves a JSON API and serving HTML doesn’t make sense?
While an empty body can absolutely be a valid choice, this example takes an initial crack at returning a Problem Details JSON.
If the client doesn’t specify that “text/html” is an accepted format, it will return JSON. That means clients that accept “/” but don’t call out “text/html” specifically will get the JSON. The accepts list uses specificity and a quality factor to rank types accepted. This middleware doesn’t care, but a future version probably should.
import Hummingbird
import Logging
struct ProblemDetail: Decodable, ResponseEncodable {
let status: Int?
let title: String
let detail: String?
init(withStatus: HTTPResponse.Status, details: String) {
self.status = withStatus.code
self.title = "\(withStatus.code): \(withStatus.reasonPhrase)"
self.detail = details
}
}
public struct JSONErrorMiddleware<Context: RequestContext>: RouterMiddleware {
public func handle(
_ request: Request, context: Context, next: (Request, Context) async throws -> Response
) async throws -> Response {
do {
return try await next(request, context)
} catch let error as HTTPError {
if let accepts = request.headers[.accept] {
if !accepts.contains("text/html") {
let problem = ProblemDetail(
withStatus: error.status,
details: "\(error) from request to \(request.uri)")
var response = try problem.response(from: request, context: context)
response.status = error.status
response.headers[.contentType] = "application/problem+json"
return response
}
}
throw error
}
}
}
Middleware for a whole Route Block
I know I will want to apply Middleware to a whole block of routes. That can be done with the trie router, but only for the happy paths. So, for example if you want to use an authentication middleware for anything filed under /private/
all the routes under /private/
that have a description will call that middleware no problem.
I saw a handful of different ways in the examples, but an addRoutes function seems to work the most reliably (e.g.):
func addRoutes(to router: Router<AppRequestContext>, atPath:RouterPath) {
router.group(atPath)
.add(middleware: AlmostEmptyMiddleware<AppRequestContext>(message: "controller method"))
.get("tryme") { _, _ in
ExampleCodable(name: "Diane", number: 3)
}
}
That middleware will only fire for the route "\(atPath)/tryme"
, not "\(atPath)/nothinghere"
If I wanted something to fire for all of "\(atPath)/"
I would have to use the ResultBuilderRouter instead of the trie and I haven’t touched that much yet at all.
Another option would be lots of little apps glued together by a Proxy server. Perhaps tricky to set up, but possibly easier to maintain in the long run since an error in the JSON api wouldn’t bring down the web server, etc. Use a data backend like postgres that can manage multiple client interactions well if that’s needed.
Summary
Middleware is the perfect place to catch and filter errors, but the information gleaned by a Middleware can only be kept around in that Middleware. Unless it gets added to a RequestContext, which will be the next Hummingbird example.
This article is part of a series.
- Part 1: How can I have Hummingbird serve static files?
- Part 2: What kinda routes can I use with Hummingbird?
- Part 3: How do I test the Hummingbird routes with swift-testing?
- Part 4: How do I get Hummingbird to serve html dynamically?
- Part 5: How do I get Hummingbird to use Mustache templates instead?
- Part 6: How do I use Hummingbird to make a basic CRUD API?
- Part 7: How do I get Hummingbird to work with the OpenAPI plugins?
- Part 8: How do I make a OpenAPI ClownAPI for Hummingbird?
- Part 9: So how exactly does Middleware come into Hummingbird?
- Part 10: This Article
- Part 11: So what even is a Hummingbird RequestContext?
- Part 12: Why would I want a Hummingbird ChildRequestContext?