How about catching errors with Hummingbird Middleware?

This article is part of a series.

This article will show four different error responses based on Middleware:

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.

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))
    }
}

screen shot of browser window that says “Return to the Route” in the default font as an H1 title and nothing after it visible.

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)
    //...
  }

screen shot of browser window that says “Error” as an H1 title and “Not Found 404” as H2 with nothing after it visible. This is all in the in the default font. No styling added.

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
    }
  }
}

screen shot of a terminal window that has the curl call and the problem detail json response showing.

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.