So what even is a Hummingbird RequestContext?
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: How about catching errors with Hummingbird Middleware?
- Part 11: This Article
- Part 12: Why would I want a Hummingbird ChildRequestContext?
A RequestContext provides a companion data layer to the Hummingbird Request type as it travels through the middleware to the api endpoint handler.
A Hummingbird Request largely echos the HTTPRequest type from HTTPTypes. If a given Hummingbird server wants to track different information that doesn’t normally get sent as part of the request, that’s a job for types conforming to the RequestContext protocol. Some of the things I would have gone to nginx or apache configuration files for can end up handled by a combination of Middlewares and RequestContexts in Hummingbird. (Although many still live at the Server level)
Most basically, use a RequestContext to override the default encoders/decoders and the maximum upload size. Key features of a context include how it can pull information up from the server layer and how it can work with Middleware.
The code in full project form (including the next post in progress) available at: https://github.com/carlynorama/HummingbirdExamples/tree/main/08_context/customContext
Step One: watch a message get passed
First thing I wanted to do is watch how information flows (and doesn’t) via middleware and a RequestContext.
All the RequestContext’s I’ve seen are structs, as is the RequestContextStorage. Information held in a context can only get passed DOWN the Middleware stack and into to the api endpoint handler. Nothing comes back up with the Response. I haven’t done any reading or experiments on storing a reference type inside a RequestContext, which could be an interesting thing to try.
The Context
I wrote both a custom RequestContext and a matching protocol to needed craft the middleware.
NOTE: When writing a middleware to be shared as a library, that protocol will be more important than the concrete type so developers can implement their own Contexts that support all the various Middlewares they need.
There’s only one special item: a noteToPass
string.
import Hummingbird
protocol ForwardingRequestContext: RequestContext {
var noteToPass: String? { get set }
}
public struct MyForwardingContext: ForwardingRequestContext {
//protocol requirement
public var coreContext: CoreRequestContextStorage
public var noteToPass: String?
public init(source: Source) {
self.coreContext = .init(source: source)
self.noteToPass = nil
}
}
The Sending Middleware
The “sending” middleware will be the middleware first listed in the stack. It gets it’s hands on the context first. In this case the middleware needs a promise that the context will have a noteToPass
variable, which gets done by providing the ForwardingRequestContext
just made, rather than the more generic RequestContext
as required by the RouterMiddleware protocol.
The handle
function needs its own var
copy of the context to make any changes since the parameter comes in as a let.
Nothing done to the context after the call to next
matters to subsequent middleware or the ultimate api endpoint handler.
import Hummingbird
struct SendingMiddleware<Context: ForwardingRequestContext>: RouterMiddleware {
public func handle(_ request: Request, context: Context, next: (Request, Context) async throws -> Response) async throws -> Response {
print("sending...")
var contextToPassForward = context
contextToPassForward.noteToPass = "Howdy future Middleware!"
let response = try await next(request, contextToPassForward)
// DON'T TRY TO DO ANYTHING TO A CONTEXT HERE.
// IT DOESN'T GO BACK "UP"
return response
}
}
Receiving Middleware
I called this the “Receiving” middleware, but the noteToPass
isn’t private. Any middleware can open it up along the way.
In the example code this will be the last middleware to touch the context before it goes into the final api endpoint handler. Here it swaps in it’s own text for the note.
import Hummingbird
struct ReceivingMiddleware<Context: ForwardingRequestContext>: RouterMiddleware {
public func handle(_ request: Request, context: Context, next: (Request, Context) async throws -> Response) async throws -> Response {
print("receiving...")
var contextToPass = context
print("Receiver got: \(context.noteToPass ?? "no message from sender")")
contextToPass.noteToPass = "Receiver says hi too!"
return try await next(request, contextToPass)
}
}
Back in Application+build
Since most of the examples have been following the Hummingbird suggested model of setting a Typealias for the context it’s super easy to swap in the new custom context up at the top-ish of Application+build.swift
//Set alias to custom RequestContext
typealias AppRequestContext = MyForwardingContext
These new middlewares get added with the sender on top:
router.addMiddleware {
SendingMiddleware()
LogRequestsMiddleware(.info)
ReceivingMiddleware()
}
But I also want to see everything that my context knows about the world when it hits the api endpoint handler. I made a type that combines my custom value with the values in the protocol required RequestContextStorage, which itself points to other sources:
struct ContextInfo: ResponseEncodable {
let noteToPass: String? //custom context
let maxUploadSize: Int //RequestContext extension
let endpointPath: String? //from CoreRequestContextStorage
let parameters: String //from CoreRequestContextStorage
let request_id: String //from Logger in RequestContextSource
let decoder:String //RequestContext associated type default
let encoder:String //RequestContext associated type default
let logger:String //from RequestContextSource protocol
}
Not shown is the context.channel
which is available to the application layer, see the IP passing example in the docs for how to get access to it in your own code.
router.get("contextSpy") { _, context -> ContextInfo in
ContextInfo(
noteToPass: context.noteToPass,
maxUploadSize: context.maxUploadSize,
endpointPath: context.endpointPath,
parameters: "\(context.parameters)",
request_id: context.id,
decoder: "\(context.requestDecoder)",
encoder: "\(context.responseEncoder)",
logger: "\(context.logger)"
)
}
Running the server and making the call…
curl -i "http://localhost:8080/contextSpy"
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Content-Length: 290
Date: Sun, 28 Sep 2025 14:05:22 GMT
Server: customContext
{"parameters":"FlatDictionary<Substring, Substring>(elements: [], hashKeys: [])","logger":"Logger(_storage: Logging.Logger.Storage)","decoder":"Foundation.JSONDecoder","endpointPath":"\/contextSpy","noteToPass":"Receiver says hi too!","encoder":"Foundation.JSONEncoder","maxUploadSize":2097152,"request_id":"53075b9f900ce1b32da2fb96a5c10332"}
The output contains the receiver’s version’s of the note.
Read A Route Parameter
What’s also notable in the output above is the empty parameters dictionary. That’s for storing any information Hummingbird extracted from route variables. Let’s revise the context to store a manipulated value based on an Integer
value passed in via the route.
First to put the var in the RequestContext:
import Hummingbird
protocol ForwardingRequestContext: RequestContext {
var noteToPass: String? { get set }
var timeConsumingData: Int? { get set }
}
public struct MyForwardingContext: ForwardingRequestContext {
public var coreContext: CoreRequestContextStorage
public var noteToPass: String?
public var timeConsumingData: Int?
public init(source: Source) {
self.coreContext = .init(source: source)
self.noteToPass = nil
self.timeConsumingData = nil
}
}
New Sending Middleware
The new sending middleware will conditionally update timeConsumingData
based on an “algorithm” that combines the potential number in the url with information from the request.
I called it timeConsumingProcess
to emphasize why creating a middleware that’s context dependant might have value. It might seem easier to keep middlewares self reliant, but every repeat process gets repeated for EVERY request that the middleware will touch. That’s a lot of extra work.
import Hummingbird
struct SendingMiddleware<Context: ForwardingRequestContext>: RouterMiddleware {
public func handle(_ request: Request, context: Context, next: (Request, Context) async throws -> Response) async throws -> Response {
print("sending...")
var contextToPassForward = context
contextToPassForward.noteToPass = "Howdy future Middleware!"
if let pathNumber = Int(context.parameters.get("number") ?? "nope.") {
let requestNumber = timeConsumingRequestProcessingTask(on: request)
contextToPassForward.timeConsumingData = pathNumber + requestNumber
}
let response = try await next(request, contextToPassForward)
return response
}
private func timeConsumingRequestProcessingTask(on request:Request) -> Int {
if let agent = request.headers[.userAgent] {
print("\(agent) wants to play. \(agent.count)")
return agent.count
}
return 732
}
}
New Receiving
The receiver will check to see if the value in the context has been previously set, and if so add… it’s 2¢.
import Hummingbird
struct ReceivingMiddleware<Context: ForwardingRequestContext>: RouterMiddleware {
public func handle(_ request: Request, context: Context, next: (Request, Context) async throws -> Response) async throws -> Response {
print("receiving...")
var contextToPass = context
print("Receiver got: \(context.noteToPass ?? "no message from sender")")
contextToPass.noteToPass = "Receiver says hi too!"
if context.timeConsumingData != nil {
contextToPass.timeConsumingData = myTwoCents(number: context.timeConsumingData!)
}
return try await next(request, contextToPass)
}
private func myTwoCents(number: Int) -> Int {
print("\(number) is a big number!")
return number + 2
}
}
New Endpoint
Adding the value to the ContextInfo structure…
struct ContextInfo: ResponseEncodable {
let timeConsumingData: Int?
let noteToPass: String?
let maxUploadSize: Int
let endpointPath: String?
let parameters: String
let request_id: String
let decoder:String
let encoder:String
let logger:String
}
And creating and endpoint that expects a value:
//http://localhost:8080/contextSpy/anythingHere
//"parameters": ... elements: [(key: \"number\", value: \"anythingHere\")] ...
router.get("contextSpy/{number}") { _, context -> ContextInfo in
ContextInfo(
timeConsumingData: context.timeConsumingData,
noteToPass: context.noteToPass,
maxUploadSize: context.maxUploadSize,
endpointPath: context.endpointPath,
parameters: "\(context.parameters)",
request_id: context.id,
decoder: "\(context.requestDecoder)",
encoder: "\(context.responseEncoder)",
logger: "\(context.logger)"
)
}
NOTE: The old endpoint will need updating, too. When it’s called the JSON just won’t have a “timeConsumingData” value and the parameters will still be empty.
Seeing the result:
curl -i "http://localhost:8080/contextSpy/354"
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Content-Length: 377
Date: Sun, 28 Sep 2025 14:26:33 GMT
Server: customContext
{"maxUploadSize":2097152,"timeConsumingData":366,"noteToPass":"Receiver says hi too!","endpointPath":"\/contextSpy\/{number}","decoder":"Foundation.JSONDecoder","request_id":"53075b9f900ce1b32da2fb96a5c10331","encoder":"Foundation.JSONEncoder","logger":"Logger(_storage: Logging.Logger.Storage)","parameters":"FlatDictionary<Substring, Substring>(elements: [(key: \"number\", value: \"354\")], hashKeys: [5772982973116696044])"}
We have a parameter!
"FlatDictionary<Substring, Substring>(elements: [(key: \"number\", value: \"354\")], hashKeys: [-6711111692066375814])"
This parameters array stores route based values only. Code meant to handle a query encoded request like:
curl -i "http://localhost:8080/urlQueryCheck?x=45&y=33"
will accesses the values via the Request:
request.uri.queryParameters
Summary
The RequestContext / Middleware pair can accomplish some pretty powerful work. That power can be sharpened by narrowing down into a ChildRequestContext for a RouteGroup once certain requirements have been met.
That’s the next post!
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: How about catching errors with Hummingbird Middleware?
- Part 11: This Article
- Part 12: Why would I want a Hummingbird ChildRequestContext?