· 6 min read

Using Hummingbird’s Request Context

Hummingbird introduces a new feature that is essential in how it integrates with other libraries: the RequestContext.

This article explains what they’re used for, and how they can help you build statically checked and performant applications.

The Core

This protocol is implemented by metadata containers. Alongside each Request, Hummingbird creates one instance of this Context. This context functions as a metadata container, and stores properties alongside the request.

The RequestContext protocol has one main requirement: It needs store a CoreRequestContextStorage.

The CoreRequestContextStorage is a container that the framework uses to store properties it needs. While the amount of properties are limited right now, this allows the framework to add more features in the future without breaking your code.

In addition, a context can specify a RequestDecoder and ResponseEncoder. These types can read the headers and body of a request, and encode a response body, respectively. This is usually backed by a codable implementation such as JSONEncoder. Since Swift bundles a JSON implementation through Foundation, this is the default.

While the en- and decoder usually focus on one content type such as JSON, it’s also possible for a context to support multiple content types.

Finally, a request specifies RequestContext/maxUploadSize, which has a sensible default value. This specifies the maximum amount of data that Hummingbird may send to a RequestDecoder before rejecting the request.

Custom Contexts

Hummingbird provides a very simple context called BasicRequestContext, which is used by default. It’s a good starting point for applications, but most applications need custom contexts.

To create a custom context, create a struct that conforms to the RequestContext protocol. This struct stores any properties related to the request.

struct CustomContext: RequestContext {
    var coreContext: CoreRequestContextStorage
    var token: String?

    init(source: ApplicationRequestContextSource) {
        self.coreContext = .init(source: source)
    }
}
HummingbirdRequestContext.swift:25

The context is not to be used for dependency injection, such as a database connection. If a property is shared between requests, inject that type in the controller instead.

From here, instantiate a Router instance using the new context as a basis.

let router = Router(context: CustomContext.self)
HummingbirdRequestContext.swift:15

Authentication

When working with Hummingbird, contexts are a very essential part of any application. It’s the glue between the framework, other libraries and routes.

So far you’ve seen the context be used to relay information on a request level between the framework, middlewares and routes. However, some libraries need to know more contextual information. For example, a JWT library can provide knowledge on the user that is making the request.

To do this, you can extend your context with a new property that stores the user information. This can be a simple struct that stores the user’s ID, or a more complex type that stores the user’s permissions.

Middleware

Middleware are powerful tools that allow intercepting requests and responses, and modify them as needed. This is a great place to add authentication, logging, or other cross-cutting concerns.

In Hummingbird, the middleware system is also designed with contexts in mind. When a request is received, the context is created and along between middleware. The middleware can then modify the context as needed.

First, create a middleware type that conforms to the RouterMiddleware protocol. Then, modify the properties in the context from the handle(_:context:next:) method.

While middleware can specify a typealias to constrain to a specific context, it’s also possible make a middleware generic.

struct SimpleAuthMiddleware: RouterMiddleware {
    typealias Context = CustomContext

    func handle(_ input: Request, context: Context, next: (Request, Context) async throws -> Output) async throws -> Response {
        var context = context
        guard
            let token = input.headers[.authorization]
        else {
            throw HTTPError(.unauthorized)
        }

        // Note: This is still not secure.
        // Token verification is missing from this example
        context.token = token

        // Pass along the chain to the next handler (middleware or route handler)
        return try await next(input, context)
    }
}
HummingbirdRequestContext.swift:36

Context Protocols

In the example above, the middleware specifies the type of Context it needs. However, using the power of Swift’s generics, it’s possible to make a middleware that works with different custom RequestContexts.

First, specify a protocol that the context must conform to. This protocol can be as simple as a marker protocol, or it can specify properties that the middleware needs.

protocol AuthContext: RequestContext {
    var token: String? { get set }
}
HummingbirdRequestContext.swift:58

Then, remove the typealias and replace it with a generic parameter. This parameter is constrained to the new protocol.

struct AuthMiddleware<Context: AuthContext>: RouterMiddleware {
    func handle(_ input: Request, context: Context, next: (Request, Context) async throws -> Output) async throws -> Response {
        var context = context
        guard
            let token = input.headers[.authorization]
        else {
            throw HTTPError(.unauthorized)
        }

        // Note: This is still not secure.
        // Token verification is missing from this example
        context.token = token

        // Pass along the chain to the next handler (middleware or route handler)
        return try await next(input, context)
    }
}
HummingbirdRequestContext.swift:64

That’s all you need to do! Now, the middleware can be used with any context that conforms to the protocol.

Conclusion

Many of Hummingbird’s features are built around the RequestContext. It’s a powerful tool that allows you to build statically checked and performant applications. By using the context, you can integrate with other libraries, add authentication, and more.

For more information, check out the Hummingbird documentation and our other tutorials! Happy coding!

Related posts

How to Build a Proxy Server with Hummingbird

Learn how to leverage the flexibility and performance of Hummingbird to build a proxy server.

Getting Started with Hummingbird

Learn how to get started with Hummingbird 2, the modern Swift Web Framework.

Working with UDP in SwiftNIO

Create UDP servers and clients using SwiftNIO and structured concurrency

Using WebSockets in Hummingbird

In this article, you will learn about WebSockets and how to use them with the Hummingbird framework in a straightforward, easy-to-follow manner.

Using OpenAPI Generator with Hummingbird

Learn how to use OpenAPI Generator to create Swift APIs with Hummingbird.

What's new in Hummingbird 2?

Discover Hummingbird 2: a Swift-based HTTP server framework, with modern concurrency and customizable request contexts.

Server-Side Swift Conference logo

ServerSide.swift

The talk recordings are live!
See you next year!

Watch the Videos