Alamofire
02 / 02

Uploads, Interceptors & Sessions

Alamofire: Uploads, Interceptors & Sessions

Uploads & Downloads

// Multipart form upload — files plus regular fields, with progress
AF.upload(
    multipartFormData: { formData in
        formData.append(imageData, withName: "avatar", fileName: "avatar.jpg", mimeType: "image/jpeg")
        formData.append("Alice".data(using: .utf8)!, withName: "name")
    },
    to: "https://api.example.com/profile"
)
.uploadProgress { progress in
    print("Upload: \(progress.fractionCompleted * 100)%")
}
.validate()
.responseDecodable(of: Profile.self) { response in }

// Downloading straight to disk (large files) instead of buffering in memory
let destination: DownloadRequest.Destination = { _, _ in
    let fileURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
        .appendingPathComponent("report.pdf")
    return (fileURL, [.removePreviousFile, .createIntermediateDirectories])
}

AF.download("https://api.example.com/report.pdf", to: destination)
    .downloadProgress { progress in print(progress.fractionCompleted) }
    .response { response in
        if let fileURL = response.fileURL { print("Saved to \(fileURL)") }
    }

Auth Token Refresh with RequestInterceptor

// A RequestInterceptor = a RequestAdapter (attach headers) +
// a RequestRetrier (decide whether/how to retry a failed request)
final class AuthInterceptor: RequestInterceptor {
    private var tokenProvider: TokenProvider

    init(tokenProvider: TokenProvider) {
        self.tokenProvider = tokenProvider
    }

    func adapt(_ urlRequest: URLRequest, for session: Session,
               completion: @escaping (Result<URLRequest, Error>) -> Void) {
        var request = urlRequest
        request.setValue("Bearer \(tokenProvider.accessToken)", forHTTPHeaderField: "Authorization")
        completion(.success(request))
    }

    func retry(_ request: Request, for session: Session, dueTo error: Error,
               completion: @escaping (RetryResult) -> Void) {
        guard request.response?.statusCode == 401 else {
            completion(.doNotRetry)
            return
        }
        tokenProvider.refreshToken { success in
            completion(success ? .retry : .doNotRetryWithError(error))
        }
    }
}

let session = Session(interceptor: AuthInterceptor(tokenProvider: tokenProvider))
session.request("https://api.example.com/me").responseDecodable(of: User.self) { response in }

Custom Session Configuration

// AF is just Session.default — create your own for scoped configuration
let configuration = URLSessionConfiguration.af.default
configuration.timeoutIntervalForRequest = 15

// Certificate pinning for sensitive endpoints
let trustManager = ServerTrustManager(evaluators: [
    "api.example.com": PinnedCertificatesTrustEvaluator()
])

let apiSession = Session(
    configuration: configuration,
    interceptor: AuthInterceptor(tokenProvider: tokenProvider),
    serverTrustManager: trustManager
)

// EventMonitor — observe every request on this Session, e.g. for debug logging
final class LoggingMonitor: EventMonitor {
    func requestDidResume(_ request: Request) {
        print("→ \(request)")
    }
}

let debugSession = Session(eventMonitors: [LoggingMonitor()])

When to Reach for Alamofire vs Plain URLSession

  • Alamofire builds on URLSession — it doesn't replace it, and caching/TLS still follow URLSession's underlying behavior.

  • Modern URLSession with async/await (`URLSession.shared.data(for:)`) covers a lot of what originally motivated Alamofire, so simple apps may not need the dependency at all.

  • Alamofire earns its keep for multipart uploads, download progress, RequestInterceptor-based auth/retry, and certificate pinning — all noticeably more boilerplate to hand-roll with raw URLSession.

  • Response handlers default to callback on the main queue, unlike raw URLSession completion handlers which always run on a background queue.

Keep your own version of these notes — editable, searchable, and organised by your stack.

Start free