Why would I want a Hummingbird ChildRequestContext?
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: So what even is a Hummingbird RequestContext?
- Part 12: This Article
Last post I made a ForwardingRequestContext and used it to save a value created by middleware.
I’ve got to admit it took me a minute to understand why and how to create a ChildRequestContext.
First the how. I honestly ran in trouble the first time I tried to make one because I just assumed it was like sub-classing and just tried to jump in and make one with out really looking that hard at the documentation. But it isn’t. The “Parent Context” is passed into the initializer as a concrete type that can be used to populate a new set of child defined custom variables. The parent does, however, provide it’s copy of the fundamental storage. That parent gets discarded after the initializer.
Most of the examples seem to have a throwing initializer for a child context. Throwing an error here can kick a client out of the app immediately.
That throwing initializer pattern leads to an answer for WHY have a ChildRequestContext, controlling access to whole groups of api endpoint handlers based on conditions checked by middleware.
To check that behavior out, I wrote RCJunior to block access to certain routes if there’s no number in the URL to seed timeConsumingData
.
The code from this post and the last in a project: https://github.com/carlynorama/HummingbirdExamples/tree/main/08_context/customContext
What is the call going to look like?
The RouterGroup initializer provides the chance to insert the child context Type
.
I’m going to show doing adding a RouterGroup full written out rather than with the common shorthand since I’ll be adding lots of comments.
//kiddiePoolRoutes will be of type RouterGroup<RCJunior>
let kiddiePoolRoutes = router.group("kiddiePool", context: RCJunior.self)
Like with middleware, the behavior instituted by the child context will only show up for defined api endpoints attached to the group (kiddiePoolRoutes
).
The Child RequestContext
RCJunior
won’t conform to ForwardingRequestContext
, but will receive a MyForwardingContext
in the initializer to satisfy the associatedtype ParentContext
of a ChildRequestContext
import Hummingbird
struct RCJunior: ChildRequestContext {
var coreContext: CoreRequestContextStorage
let magicNumber: Int
//expects concrete type.
init(context parentContext: MyForwardingContext) throws {
self.coreContext = parentContext.coreContext
guard parentContext.timeConsumingData != nil else {
//https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Status/418
throw HTTPError.init(.init(code: 418, reasonPhrase: "I am a teapot."))
}
self.magicNumber = parentContext.timeConsumingData!
}
}
Unlike timeConsumingData
, magicNumber
isn’t an optional. If timeConsumingData
isn’t set in the parent this child context will fail to initialize. What does that mean? The context will halt progress and throw an error back up the middleware chain to be turned into the server’s response. All the middleware added to the router before the group gets declared will will run. None of the middleware added to the group will run.
I picked an entertaining to me error message that means “I’m a teapot. Find a coffee maker.” (It’s an old code…).
A ChildContextInfo
would still have everything from the parent context’s storage, but none of it’s custom variables. Only it’s own.
struct ChildContextInfo: ResponseEncodable {
let magicNumber: Int
let maxUploadSize: Int
let endpointPath: String?
let parameters: String
let request_id: String
let decoder:String
let encoder:String
let logger:String
}
An api endpoint that fails
Adding an api endpoint that is not expected to work:
let kiddiePoolRoutes = router.group("kiddiePool", context: RCJunior.self)
kiddiePoolRoutes.get { _, context -> String in
print("child route without \"number\"")
return "CODE: \(context.magicNumber)"
}
Call it…
curl -i "http://localhost:8080/kiddiePool"
BONK!
HTTP/1.1 418 I'm a teapot
Content-Length: 0
Date: Sun, 28 Sep 2025 18:34:26 GMT
Server: customContext
An api endpoint that works
kiddiePoolRoutes.get("{number}") { _, context -> String in
print("child route with \"number\"")
return "CODE: \(context.magicNumber)"
}
curl -i "http://localhost:8080/kiddiePool/354"
HTTP/1.1 200 OK
Content-Type: text/plain; charset=utf-8
Content-Length: 9
Date: Sun, 28 Sep 2025 18:43:29 GMT
Server: customContext
CODE: 366
Other api endpoints that work
Any route added to kiddiePool routes will have to have a /{number}/
in their path somewhere to have a chance of of working.
kiddiePoolRoutes.get("/magic/{number}/**") { request, context -> String in
print("description:\(request.uri.description)")
//catches the unnamed
print("parameters:\(context.parameters.getCatchAll())")
return "CODE: \(context.magicNumber)"
}
curl -i "http://localhost:8080/kiddiePool/magic/3/rabbit/saw/cards"
HTTP/1.1 200 OK
Content-Type: text/plain; charset=utf-8
Content-Length: 8
Date: Sun, 28 Sep 2025 18:59:31 GMT
Server: customContext
CODE: 15
The log reads:
## printed by first middleware before twoCents in second
## it's the result of number + userAgent.count
13 is a big number!
description:/kiddiePool/magic/3/rabbit/saw/cards
parameters:["rabbit", "saw", "cards"]
Don’t forget to test!
@Test func testKiddiePool() async throws {
let app = try await buildApplication(TestArguments())
//base, nothing else
try await app.test(.router) { client in
let _ = try await client.execute(uri: "/kiddiePool/", method: .get) { response in
print(response.status)
#expect(response.status.code == 418)
}
}
//with number
let number = 65
try await app.test(.router) { client in
let _ = try await client.execute(uri: "/kiddiePool/\(number)", method: .get) { response in
#expect(response.status == .ok)
let decoded = String(bytes: response.body.readableBytesView, encoding: .utf8 )
//Hummingbird tests suite does not provide a User Agent.
let expectedTotal = 2+732+number
#expect(decoded == "CODE: \(expectedTotal)")
}
}
//something in the path that is not a number
try await app.test(.router) { client in
let _ = try await client.execute(uri: "/kiddiePool/notANumber", method: .get) { response in
print(response.status)
#expect(response.status.code == 418)
}
}
//undefined route
try await app.test(.router) { client in
let _ = try await client.execute(uri: "/kiddiePool/notANumber/and/too/much/else", method: .get) { response in
print(response.status)
#expect(response.status == .notFound)
}
}
//magic path success
let magicNumber = 3
try await app.test(.router) { client in
let _ = try await client.execute(uri: "/kiddiePool/magic/\(magicNumber)/rabbit/saw/cards", method: .get) { response in
print(response.status)
#expect(response.status == .ok)
let decoded = String(bytes: response.body.readableBytesView, encoding: .utf8 )
#expect(decoded != nil)
let expectedTotal = 2+732+magicNumber
#expect(decoded == "CODE: \(expectedTotal)")
}
}
//magic path no number
try await app.test(.router) { client in
let _ = try await client.execute(uri: "/kiddiePool/magic/notANumber/rabbit/saw/cards", method: .get) { response in
print(response.status)
#expect(response.status.code == 418)
}
}
//magic path, number, but no followers
//** requires something.
try await app.test(.router) { client in
let _ = try await client.execute(uri: "/kiddiePool/magic/\(magicNumber)/", method: .get) { response in
print(response.status)
#expect(response.status == .notFound)
}
}
}
An extra note on Middleware
Something I was able to confirm while working on getting a child context right is that the order middleware gets added to a group matters, not just to the other middleware, but to the api endpoints as well.
let kiddiePoolRoutes = router.group("kiddiePool", context: RCJunior.self)
//applies to everything after, so everything in "kiddiePoolRoutes"
kiddiePoolRoutes.add(middleware: AlmostEmptyMiddleware(message: "kiddiePool A"))
//curl -i "http://localhost:8080/kiddiePool/"
kiddiePoolRoutes.get { _, context -> String in
print("child route without \"number\"")
return "CODE: \(context.magicNumber)"
}
//curl -i "http://localhost:8080/kiddiePool/354"
kiddiePoolRoutes.get("{number}") { _, context -> String in
print("child route with \"number\"")
return "CODE: \(context.magicNumber)"
}
//this middleware will apply to
//kiddiePoolRoutes.get("/magic/{number}/**") only.
kiddiePoolRoutes.add(middleware: AlmostEmptyMiddleware(message: "kiddiePool B"))
//curl -i "http://localhost:8080/kiddiePool/magic/3/rabbit/saw/cards"
kiddiePoolRoutes.get("/magic/{number}/**") { request, context -> String in
print("description:\(request.uri.description)")
print("parameters:\(context.parameters.getCatchAll())")
return "CODE: \(context.magicNumber)"
}
Summary
The RCJunior
ChildRequestContext uses an optional value in it’s ParentContext to determine if a client will get access to the API endpoints it applies to. Understanding this simple model will hopefully make it easier to sort through the workings of an actually-good auth library, hummingbird-auth.
That probably won’t be the next Hummingbird post, but it’s on the list.
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: So what even is a Hummingbird RequestContext?
- Part 12: This Article