So what even is a Hummingbird RequestContext?

This article is part of a series.

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.