HTTP in Swift, Part 16: Composite Loaders

Part 16 in a series on building a Swift HTTP framework:

  1. HTTP in Swift, Part 1: An Intro to HTTP
  2. HTTP in Swift, Part 2: Basic Structures
  3. HTTP in Swift, Part 3: Request Bodies
  4. HTTP in Swift, Part 4: Loading Requests
  5. HTTP in Swift, Part 5: Testing and Mocking
  6. HTTP in Swift, Part 6: Chaining Loaders
  7. HTTP in Swift, Part 7: Dynamically Modifying Requests
  8. HTTP in Swift, Part 8: Request Options
  9. HTTP in Swift, Part 9: Resetting
  10. HTTP in Swift, Part 10: Cancellation
  11. HTTP in Swift, Part 11: Throttling
  12. HTTP in Swift, Part 12: Retrying
  13. HTTP in Swift, Part 13: Basic Authentication
  14. HTTP in Swift, Part 14: OAuth Setup
  15. HTTP in Swift, Part 15: OAuth
  16. HTTP in Swift, Part 16: Composite Loaders

So far we’ve built two different loaders that handle authentication, and it’s conceivable we’d want to build more to support others. Wouldn’t it be nice if we could encapsulate all of the “authentication” logic into a single loader?

We’re going to do that by creating a composite loader.

The Setup

This loader will be similar in structure to other loaders we’ve made: the bulk of the work will happen in the load(task:) method, and we’ll need to implement reset(with:) at some point too.

public class Auth: HTTPLoader {

    private let basic: BasicAuth = ...
    private let oauth: OAuth = ...

    public override func load(task: HTTPTask) {
        if /* should load with basic auth */ {
            basic.load(task: task)
        } else if /* should load with oauth */ {
            oauth.load(task: task)
        } else {
            super.load(task: task)
        }
    }

    public override func reset(with group: DispatchGroup) {
        basic.reset(with: group)
        oauth.reset(with: group)
        super.reset(with: group)
    }

}

This looks pretty good, right? Well, it’s not 😅. There are a couple of problems with this approach that we’ll need to fix:

  1. We need a way to determine which loader should load a particular request.

  2. There’s a severe logic flaw with the reset(with:) method.

Choosing Loaders for Requests

Given an incoming HTTPTask, we need a way to determine how it should be authenticated. Or in other words, each request needs to tell us what it wants. Fortunately, we already have a way to specify per-request options!

public struct AuthenticationMethod: Hashable, HTTPRequestOption {
    // by default, requests are not authenticated
    public static let defaultOptionValue: AuthenticationMethod? = nil

    public static let basic = AuthenticationMethod(rawValue: "basic")
    public static let oauth = AuthenticationMethod(rawValue: "oauth")

    public let rawValue: String

    public init(rawValue: String) { self.rawValue = rawValue }
}

extension HTTPRequest {    
    public var authenticationMethod: AuthenticationMethod? {
        get { self[option: AuthenticationMethod.self] }
        set { self[option: AuthenticationMethod.self] = newValue }
    }
}

I’ve chosen to use a struct for the AuthenticationMethod type to make it so that clients can create their own AuthenticationMethods to correspond to their own custom authentication loaders; an enum would make that very difficult. We can also update the loader to allow clients to pass in the kinds of authenticating loaders they want to use, by creating a Dictionary<AuthenticationMethod, HTTPLoader> to associate an authentication method to a particular loader:

public class Auth: HTTPLoader {

    private let subloaders: [AuthenticationMethod: HTTPLoader]

    public init(loaders: [AuthenticationMethod: HTTPLoader]) {
        self.subloaders = loaders
        super.init()
    }

    public override func load(task: HTTPTask) {
        if let method = task.request.authenticationMethod {
            // the request wants authentication

            if let loader = subloaders[method] {
                // we know which loader to use
                loader.load(task: task)
            } else {
                // we don't know which loader to use
                task.fail(.cannotAuthenticate)
            }
        } else {
            // no authentication; immediately pass it on
            super.load(task: task)
        }
    }

    public override func reset(with group: DispatchGroup) {
        subloaders.values.forEach { $0.reset(with: group) }
        super.reset(with: group)
    }

}

Food for thought: This implementation will fail a task if it can’t determine how to authenticate it. Is that right decision to make? Why or why not?

This way our Auth loader not only supports built-in authentication methods (BasicAuth and OAuth); it also supports any kind of AuthenticationMethod and custom authenticating loader that a client of the framework makes. Another huge advantage of this approach is that the Auth loader does not need to intercept any delegate methods of the underlying BasicAuth or OAuth loaders. Since those loaders are now created externally, the creator can set the delegate (for reading/writing credentials, etc) to whatever object it wants. If the Auth loader had created those values itself, we would’ve needed a way to either inject a delegate or intercept the delegate method calls before forwarding them on via a different delegate protocol.

The Reset Race

There’s an interesting problem with the reset(with:) method. Let’s take a look at how the nextLoader values are configured with this kind of composite loader:

The reset connections for a composite loader

Our inner loaders need a .nextLoader value, because they need a way to pass their modified tasks down the chain for eventual transmission over the network. However, if we send them straight on to the Auth loader’s .nextLoader, then we end up with a situation where that next loader will have its reset(with:) method invoked multiple times during a single reset attempt. We can visualize this happening:

  • the loader before Auth gets a reset call and begins resetting. It instructs Auth to reset
  • Auth instructs every child loader to reset, and also instructs its next loader to reset
  • Each child loader begins resetting and each instructs its next loader to reset, but Auth already told that loader to reset

Therefore, we need to intercept all of these reset calls from the inner loader and stop them from propagating down the chain. Fortunately, we already have a loader that does that! Back in part 9, we built a ResetGuard loader that does exactly that.

Broadly speaking, the Auth loader needs to make sure that every child loader’s .nextLoader points to a private ResetGuard loader. Then it needs to intercept the call to set its own nextLoader and instead make that the next loader of the ResetGuard. In this manner, the Auth loader is injecting a new ResetGuard loader instance after itself in the overall loading chain.

I’ll leave this up to you to implement.

The 'Get out of jail free' card from monopoly with the description that 'this code is left as an exercise to the reader'

Conclusion

Overall, building a composite loader is a conceptually straight-forward task. There’s the interesting edge case about the nextLoader we need to handle, but once we understand the problem, the solution presents itself with components we’ve already built.

In the next post, we’ll be looking at a different kind of composite loader to support a variant on OAuth: OpenID.


Related️️ Posts️

HTTP in Swift, Part 15: OAuth
HTTP in Swift, Part 14: OAuth Setup
HTTP in Swift, Part 13: Basic Authentication
HTTP in Swift, Part 12: Retrying
HTTP in Swift, Part 11: Throttling
HTTP in Swift, Part 10: Cancellation
HTTP in Swift, Part 9: Resetting
HTTP in Swift, Part 8: Request Options
HTTP in Swift, Part 7: Dynamically Modifying Requests
HTTP in Swift, Part 6: Chaining Loaders
HTTP in Swift, Part 5: Testing and Mocking
HTTP in Swift, Part 4: Loading Requests
HTTP in Swift, Part 3: Request Bodies
HTTP in Swift, Part 2: Basic Structures
HTTP in Swift, Part 1: An Intro to HTTP