So how exactly does Middleware come into Hummingbird?

This article is part of a series.

So previous examples are already using Hummingbird provided Middleware.

Other Middlewares provided include:

Hummingbird also provides some Middleware protocols to help get common tasks started:

Not to mention the many examples in the various repos of the Hummingbird Project itself.

But what if you want to write your own?

I’m going to show 3 over the next 3 posts:

Middleware

This middleware isn’t strictly needed for logging request header information because the Logging middleware already handles it in a far more robust and ergonomic way.

        LogRequestsMiddleware(.info, includeHeaders: .some([.contentType]))
        LogRequestsMiddleware(.info, includeHeaders: .all())
        LogRequestsMiddleware(.info, includeHeaders: .all(except: [.cookie]))

What this middleware will show is also logging the response headers, albeit in a much more rudimentary way.

Middleware requires a handler that calls the next middleware on the stack, but since that call happens inside the handler, that method can also operate on the Response bubbling up as well.

public struct EmptyMiddleware<Context: RequestContext>: RouterMiddleware {
    public func handle(_ request: Request, context: Context, next: (Request, Context) async throws -> Response) async throws -> Response {
        return try await next(request, context)
    }
}

So if I wanted to write a middleware that would show the headers for BOTH the request and the response it might look something like:

import Hummingbird
import Logging

public struct LogHeadersMiddleware<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 {

        let requestHeaders = formatHeaderForLog(headers: request.headers)
        context.logger.log(level: level, Logger.Message(stringLiteral: "\nHeaders for \(request.uri.path) request:\n\(requestHeaders)"))

        //return Response
        let response = try await next(request, context)

        let responseHeaders = formatHeaderForLog(headers: response.headers)
        context.logger.log(level: level, Logger.Message(stringLiteral: "\nResponding with:\n\(responseHeaders)"))

        return response
    }
}

The output for a curl request to the /hello route in the demo (curl "http://localhost:8080/hello") looks like:

2025-09-26T06:20:35-0700 info inthemiddle: hb.request.id=2f72a050b4afa3a2698b886b64654226 [inthemiddle] 
Headers for /hello request:
        User-Agent:curl/8.7.1
        Accept:*/*
------
2025-09-26T06:20:35-0700 info inthemiddle: hb.request.id=2f72a050b4afa3a2698b886b64654226 [inthemiddle] 
Responding with:
        Content-Type:text/plain; charset=utf-8
        Content-Length:5
------

Order Matters

Let’s see what happens with the middleware called from two different places: right after the normal logger and at the end.

First right after the usual logger.

    router.addMiddleware {
        // logging middleware
        LogRequestsMiddleware(.info)
        //logs the headers of the request and the responses
        LogHeadersMiddleware(level: .info)
        //serves the static files in public folder by default.
        FileMiddleware(searchForIndexHtml: true)
    }

Calling the home page which has a static HTML page available (curl "http://localhost:8080/") the built in Logger prints out first and the custom logger prints out everything second. If the FileMiddleware logged anything, it would have shown up where noted in the added comment.

2025-09-26T06:31:44-0700 info inthemiddle: hb.request.id=94e54b3188d6f6c807d5eed484bca2cb hb.request.method=GET hb.request.path=/ [Hummingbird] Request
2025-09-26T06:31:44-0700 info inthemiddle: hb.request.id=94e54b3188d6f6c807d5eed484bca2cb [inthemiddle] 
Headers for / request:
        User-Agent:curl/8.7.1
        Accept:*/*
------
# FileMiddleware logging would have happened here! 
2025-09-26T06:31:44-0700 info inthemiddle: hb.request.id=94e54b3188d6f6c807d5eed484bca2cb [inthemiddle] 
Responding with:
        Content-Length:364
        Last-Modified:Tue, 16 Sep 2025 16:40:02 GMT
        ETag:W/"0900180b0600303830322e3932373739"
        Content-Type:text/html
        Accept-Ranges:bytes
------

Now move LogHeadersMiddleware AFTER the file middleware.

    router.addMiddleware {
        // logging middleware
        LogRequestsMiddleware(.info)
        //serves the static files in public folder by default.
        FileMiddleware(searchForIndexHtml: true)
        //logs the headers of the request and the responses
        LogHeadersMiddleware(level: .info)
    }

There’s NOTHING for the response because there isn’t one yet. Nothing in the Hummingbird stack has prepped one because there isn’t a route for “/” in the router at all. Not until it goes back up through the FileMiddleware will it have the index page Response in hand.

2025-09-26T06:36:24-0700 info inthemiddle: hb.request.id=eb458259999c78dc5069ba5bc9205f41 [inthemiddle] 
Headers for / request:
        User-Agent:curl/8.7.1
        Accept:*/*
------

The next test is to move the middleware to the top and call a known undefined path (no route, no static page.) (curl "http://localhost:8080/knownempty")

    router.addMiddleware {
        //logs the headers of the request and the responses
        LogHeadersMiddleware(level: .info)
        // logging middleware
        LogRequestsMiddleware(.info)
        //serves the static files in public folder by default.
        FileMiddleware(searchForIndexHtml: true)

    }

It’s exactly the same, request headers print and nothing for response headers.

2025-09-26T06:42:48-0700 info inthemiddle: hb.request.id=eb458259999c78dc5069ba5bc9205f43 [inthemiddle] 
Headers for /knownempty request:
        User-Agent:curl/8.7.1
        Accept:*/*
------

The curl call printed out nothing as well. That will change if change the curl call to (curl -i "http://localhost:8080/knownempty"), in which case the logs will not print out a response, but the curl will be:

HTTP/1.1 404 Not Found
Content-Length: 0
Date: Fri, 26 Sep 2025 15:49:48 GMT
Server: inthemiddle

So when does Hummingbird formulate that Response? There is an HTTPError (docs) type that gets propagated up instead of a Response as part of the try in the try await next(request, context) call. The not found error gets thrown in the respond method by the notFoundResponder of the RouterResponder’s that the application builds from the Router passed to it at startup.

So how to catch it and swap in something else? More middleware!

Summary

LogHeadersMiddleware demonstrates interceding on a path to punt out headers to the logging system. The next demo will show how to catch undefined routes to return appropriate 404 responses.

This article is part of a series.