Why would I want a Hummingbird ChildRequestContext?

This article is part of a series.

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!

Tests

@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.