How Does Alamofire Make HTTP Requests?
May 24, 2018

We commonly say that you should only use libraries that you thoroughly understand but we rarely take the time to really dig into those libraries to see how they work. How exactly does Alamofire use a URLRequest or a URL String to make a network call?

Dev working from home
(image by @ThoughtCatalog)

This tutorial uses Swift 4 and Alamofire 4.7.

The syntax to make a networking request makes it a little difficult to guess what’s happening within Alamofire. If we’re providing a URL string it looks like this:


Alamofire.request(myURLString)
    .responseJSON { response in
      // do stuff with the JSON or error
    }

Or we could provide a URLRequest:


let url = URL(string: myURLString)!
let urlRequest = URLRequest(url: url)
Alamofire.request(urlRequest)
    .responseJSON { response in
      // do stuff with the JSON or error
    }

The same thing would look like this if we were using URLSession directly:


let url = URL(string: myURLString)!
let urlRequest = URLRequest(url: url)
let session = URLSession.shared
let task = session.dataTask(with: urlRequest) {
    (data, response, error) in
    // serialize JSON
    // do stuff with the JSON or error
}
task.resume()

Once we’ve set up the URLRequest and the URLSession, we’re creating a dataTask with it then using resume() to send it. The completion handler for the dataTask lets us work with the results of the call. That’s where we could use JSONSerialization to convert the results to JSON or handle any errors.

Since Alamofire is a wrapper around URLSession there should be code in Alamofire there that creates a dataTask then sends it using .resume(). So let’s look at the Alamofire code to see if we can figure out how that actually happens.

What does Alamofire.request(…) do?

Alamofire.request(myURLString) is a function call. To see the code for that function, mouse over it in Xcode then cmd-click on it or right-click and select “Jump to Definition”.

Right-click and choose Jump to Definition
Right-click and choose Jump to Definition

The definition is in Alamofire.swift and it looks like this for the URLRequest version of Alamofire.request:


public func request(_ urlRequest: URLRequestConvertible) -> DataRequest {
    return SessionManager.default.request(urlRequest)
}

There’s a similar version for the URL String version of Alamofire.request.


@discardableResult
public func request(
    _ url: URLConvertible,
    method: HTTPMethod = .get,
    parameters: Parameters? = nil,
    encoding: ParameterEncoding = URLEncoding.default,
    headers: HTTPHeaders? = nil)
    -> DataRequest {
    return SessionManager.default.request(
        url,
        method: method,
        parameters: parameters,
        encoding: encoding,
        headers: headers
    )
}

Which shows us all of the optional arguments: method, parameters, encoding, and headers. We can use those to make other types of HTTP requests.

The underlying code is basically the same for both versions of Alamofire.request so we’ll focus on the URL String version.

All that this fuction does is call a similar function on the default SessionManager with all of the optional arguments set. To see it right-click and select “Jump to Definition” (or cmd-click) again:


@discardableResult
open func request(
    _ url: URLConvertible,
    method: HTTPMethod = .get,
    parameters: Parameters? = nil,
    encoding: ParameterEncoding = URLEncoding.default,
    headers: HTTPHeaders? = nil)
    -> DataRequest {
    var originalRequest: URLRequest?

    do {
        originalRequest = try URLRequest(url: url, method: method, headers: headers)
        let encodedURLRequest = try encoding.encode(originalRequest!, with: parameters)
        return request(encodedURLRequest)
    } catch {
        return request(originalRequest, failedWith: error)
    }
}

Protip: request(...) returns a DataRequest. That’s an Alamofire class that inherits from Alamofire.Request. Alamofire.Request has a very handy feature: the debugDescription returns a cURL statement that’s equivalent to the Alamofire request. So if you’re having trouble debugging an API call in your app, use let request = Alamofire.request(...) then debugPrint(request) after the completion handler(s). Then you can compare the cURL statement to your API docs, share it with a backend dev to see what’s wrong, or paste it into Terminal so you can tweak it there to figure out what you should be sending.

What’s the Session Manager

The SessionManager is what really does the work in Alamofire. Calls like Alamofire.request(...) are just convenient short-hand for similar calls to the default SessionManager like SessionManager.default.request(...). This code:


Alamofire.request(myURLString)
  .responseJSON { response in
    // do stuff with the JSON or error
  }

Does the same thing as this:


let sessionManager = Alamofire.SessionManager.default
sessionManager.request(myURLString)
  .responseJSON { response in
    // do stuff with the JSON or error
  }

You can create a non-default SessionManager if you want to use URLSessionConfiguration to set up your session. For example, you can use it to create a background session or to set default headers that should be included with all network calls in the session.

For more details, see the SessionManager docs.

Now, back to digging into the Alamofire code to figure out what’s happening when we call Alamofire.request(...).

What does SessionManager.default.request(…) do?

Here’s the request function that we’ve dug down to:


@discardableResult
open func request(
    _ url: URLConvertible,
    method: HTTPMethod = .get,
    parameters: Parameters? = nil,
    encoding: ParameterEncoding = URLEncoding.default,
    headers: HTTPHeaders? = nil)
    -> DataRequest {
    var originalRequest: URLRequest?

    do {
        originalRequest = try URLRequest(url: url, method: method, headers: headers)
        let encodedURLRequest = try encoding.encode(originalRequest!, with: parameters)
        return request(encodedURLRequest)
    } catch {
        return request(originalRequest, failedWith: error)
    }
}

request(...) in SessionManager creates a URLRequest with all of the inputs you provided, including encoding parameters. Then it just returns it. That doesn’t seem to do much…

But notice that it creates a DataRequest like return request(encodedURLRequest) or return request(originalRequest, failedWith: error). Cmd-click to see what request does there.


@discardableResult
open func request(_ urlRequest: URLRequestConvertible) -> DataRequest {
    var originalRequest: URLRequest?

    do {
        originalRequest = try urlRequest.asURLRequest()
        let originalTask = DataRequest.Requestable(urlRequest: originalRequest!)

        let task = try originalTask.task(session: session, adapter: adapter, queue: queue)
        let request = DataRequest(session: session, requestTask: .data(originalTask, task))

        delegate[task] = request

        if startRequestsImmediately { request.resume() }

        return request
    } catch {
        return request(originalRequest, failedWith: error)
    }
}

If you dug down through the URL string version of Alamofire.request then you’d get to this point too.

This function takes a URLRequest (or at least something that can easily be converted to one, see the discussion of URLRequestConvertible in this post for details).

urlRequest.asURLRequest() converts whatever was passed in into a URLRequest. E.g., if you passed a URL string like https://grokswift.com you’d end up with a URLRequest to make a GET request to that URL with no parameters, no non-default headers, and no encoding.

Then it creates a DataRequest.Requestable(...) and calls originalTask.task(...) on it. Let’s cmd-click on Requestable to see what those two calls do before continuing on with the function we’ve been looking at:


struct Requestable: TaskConvertible {
    let urlRequest: URLRequest

    func task(session: URLSession, adapter: RequestAdapter?, queue: DispatchQueue) throws -> URLSessionTask {
        do {
            let urlRequest = try self.urlRequest.adapt(using: adapter)
            return queue.sync { session.dataTask(with: urlRequest) }
        } catch {
            throw AdaptError(error: error)
        }
    }
}

There’s nothing special in the DataRequest.Requestable(...) initializer, it’s just the default member-wise struct initializer so it just sets the value of urlRequest.

func task(...) looks like we’re getting closer to where the magic happens.

self.urlRequest.adapt(using: adapter) is neat it’s but not what we’re focused on right now. RequestAdapter lets you tweak URLRequests before they get sent.

session.dataTask(with: urlRequest) is exactly the code we’ve been looking for. It’s creating a dataTask with a URLSession. But it’s not immediately sending it since there’s no call to .resume(). Instead it’s being done within queue.sync { ... }.

The queue is being passed in when task(...) is called:


let task = try originalTask.task(session: session, adapter: adapter, queue: queue)

It’s part of SessionManager and is declared as:


let queue = DispatchQueue(label: "org.alamofire.session-manager." + UUID().uuidString)

It’s a shared queue for the Alamofire session (unless you’ve passed in a custom one). queue.sync { ... } executes the contents of the code block (the stuff between { and }) and waits for it to finish. It prevents multiple calls like that happening at the same time.

At this point we’ve found where the dataTask is created but not where it’s sent using resume().

Back up to SessionManager.default.request(…)

Now that we know what happens when DataRequest.Requestable is called, let’s figure out the rest of SessionManager.default.request(...):


open func request(_ urlRequest: URLRequestConvertible) -> DataRequest {
    var originalRequest: URLRequest?

    do {
        originalRequest = try urlRequest.asURLRequest()
        let originalTask = DataRequest.Requestable(urlRequest: originalRequest!)

        let task = try originalTask.task(session: session, adapter: adapter, queue: queue)
        let request = DataRequest(session: session, requestTask: .data(originalTask, task))

        delegate[task] = request

        if startRequestsImmediately {
            request.resume()
        }

        return request
    } catch {
        return request(originalRequest, failedWith: error)
    }
}

Before running the task, it gets packed up in an Alamofire.Request:

let request = DataRequest(session: session, requestTask: .data(originalTask, task))

And stored it by giving it to the delegate:


delegate[task] = request

By default, startsRequestImmediately is true:


open var startRequestsImmediately: Bool = true

So request.resume() gets called. That looks like the other half of the URLSession code that we’re looking for but request is an Alamofire DataRequest, not a URLRequest. To find where the dataTask gets sent using resume(), we need to look at the definition of DataRequest.resume():


open func resume() {
    guard let task = task else { 
        delegate.queue.isSuspended = false
        return
    }

    if startTime == nil {
        startTime = CFAbsoluteTimeGetCurrent()
    }

    task.resume()

    NotificationCenter.default.post(
        name: Notification.Name.Task.DidResume,
        object: self,
        userInfo: [Notification.Key.Task: task]
    )
}

After checking that it has a task, the startTime gets recorded and task.resume() is called.

task in this case is a property of Request:


open var task: URLSessionTask? { return delegate.task }

It gets the task from the delegate, where we just stored it.

So that task.resume() is the other half of that URLSession code that we’ve been looking for!

To summarize, the URLSession.dataTask is created by the SessionManager like:


originalTask.task(session: session, adapter: adapter, queue: queue)

Then sent by:


if startRequestsImmediately {
    request.resume()
}

Which calls task.resume() in Alamofire.Request.

As long as the queue isn’t suspended and startRequestsImmediately is true.

Finally, a notification gets posted to let anyone who is interested know that this task has been resumed. (Remember, .resume() can start a dataTask as well as resuming one that’s been paused.)

So we’ve figured out how calling Alamofire.request ends up making a networking request using URLSession.dataTask.

What if startRequestsImmediately is false

If startRequestsImmediately isn’t true then the SessionManager won’t fire off the request. But Alamofire.request(...) returns the DataRequest so you can start the request like this:


let request = Alamofire.request(myURLString)
    .responseJSON { response in
      // do stuff with the JSON or error
    }
request.resume()

What does the delegate do?

We didn’t really look at this line in the SessionManager.default.request(...) function:


delegate[task] = request

delegate is a SessionDelegate (again, cmd-click to see where it’s defined):


open let delegate: SessionDelegate

According to the docs:

By default, an Alamofire SessionManager instance creates a SessionDelegate object to handle all the various types of delegate callbacks that are generated by the underlying URLSession.

The SessionDelegate lets you get more control over what happens when sending network requests. It has a few closures that you can override to provide custom handling for things like authentication challenges, background sessions finishing all their events, HTTP redirection, caching results from a networking call, …

TL; DR

In short, here’s what we figured out:

  1. Alamofire.request(...) gets called
  2. It asks the SessionManager to actually do it
  3. SessionManager creates a URLRequest
  4. A DataRequest gets created
  5. You get a chance to change the URLRequest using RequestAdapter
  6. The DataRequest creates a dataTask
  7. The SessionDelegate keeps track of the DataRequest and dataTask
  8. If startRequestsImmediately is true, the SessionManager starts the dataTask
  9. The DataRequest is returned to the caller, so they can start it if startRequestsImmediately is false (or pause it or cancel it or whatever)

And that’s how Alamofire sends networking requests.

And that’s how it works!

We’ve figured out how Alamofire makes network calls using the URLSession functions. Next time we’ll look at how we actually get the data in the response in the response handlers.

If you’d like more Swift tutorials on topics like this one, sign up below to get them sent directly to your inbox.

Want more Swift tutorials like this one?

Sign up to get the latest GrokSwift tutorials and information about GrokSwift books sent straight to your inbox

Other Posts You Might Like