So how exactly does Middleware come into Hummingbird?
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: This Article
- Part 10: How about catching errors with Hummingbird Middleware?
- Part 11: So what even is a Hummingbird RequestContext?
- Part 12: Why would I want a Hummingbird ChildRequestContext?
So previous examples are already using Hummingbird provided Middleware.
- Logging Middleware which determines what gets logged out to the terminal while running, a skin around swift-log
- File Middleware which serves up static pages when there’s no route for the same path defined in the Router
Other Middlewares provided include:
- CORS Middleware for managing Cross-origin resource sharing
- Metrics Middleware The same way the logging middleware wraps swift-log this wraps swift-metrics
- Tracing Provides a tracing interface distributed tracing (W3 working group)
Hummingbird also provides some Middleware protocols to help get common tasks started:
- AuthenticatorMiddleware
- A whole Authentication framework provided as a library as well.
- SessionMiddleware
- RequestDecompression and ResponseCompression Middlewares
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:
- a middleware that will log out header information
- one that falls back to an error page
- one that saves information in the context for later use
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.
- 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: This Article
- Part 10: How about catching errors with Hummingbird Middleware?
- Part 11: So what even is a Hummingbird RequestContext?
- Part 12: Why would I want a Hummingbird ChildRequestContext?