Table of contents

How do I implement custom network adapters with Alamofire?

Custom network adapters in Alamofire provide a powerful way to intercept, modify, and handle HTTP requests and responses at a low level. By implementing custom adapters, you can add advanced functionality like request transformation, response caching, authentication handling, and custom retry logic. This comprehensive guide will show you how to create and implement custom network adapters effectively.

Understanding Alamofire's Session Architecture

Alamofire uses a session-based architecture where Session acts as the central coordinator for HTTP requests. Custom adapters work by implementing the RequestAdapter and RequestRetrier protocols, allowing you to customize request handling behavior.

Basic Request Adapter Implementation

Here's how to create a basic custom request adapter:

import Alamofire
import Foundation

class CustomRequestAdapter: RequestAdapter {
    private let apiKey: String
    private let baseURL: String

    init(apiKey: String, baseURL: String) {
        self.apiKey = apiKey
        self.baseURL = baseURL
    }

    func adapt(_ urlRequest: URLRequest, for session: Session, completion: @escaping (Result<URLRequest, Error>) -> Void) {
        var adaptedRequest = urlRequest

        // Add custom headers
        adaptedRequest.setValue("Bearer \(apiKey)", forHTTPHeaderField: "Authorization")
        adaptedRequest.setValue("application/json", forHTTPHeaderField: "Content-Type")
        adaptedRequest.setValue("iOS/14.0", forHTTPHeaderField: "User-Agent")

        // Modify URL if needed
        if let url = adaptedRequest.url,
           let newURL = URL(string: baseURL + url.path) {
            adaptedRequest.url = newURL
        }

        completion(.success(adaptedRequest))
    }
}

Creating Advanced Request Adapters

Authentication and Token Management Adapter

For applications requiring dynamic authentication tokens, implement an adapter that handles token refresh:

class TokenAuthAdapter: RequestAdapter {
    private var accessToken: String?
    private let tokenRefreshService: TokenRefreshService
    private let queue = DispatchQueue(label: "tokenAdapter", qos: .utility)

    init(tokenRefreshService: TokenRefreshService) {
        self.tokenRefreshService = tokenRefreshService
    }

    func adapt(_ urlRequest: URLRequest, for session: Session, completion: @escaping (Result<URLRequest, Error>) -> Void) {
        queue.async { [weak self] in
            guard let self = self else {
                completion(.failure(AdapterError.selfDeallocated))
                return
            }

            // Check if token needs refresh
            if self.shouldRefreshToken() {
                self.tokenRefreshService.refreshToken { result in
                    switch result {
                    case .success(let newToken):
                        self.accessToken = newToken
                        completion(.success(self.addAuthHeader(to: urlRequest, token: newToken)))
                    case .failure(let error):
                        completion(.failure(error))
                    }
                }
            } else if let token = self.accessToken {
                completion(.success(self.addAuthHeader(to: urlRequest, token: token)))
            } else {
                completion(.success(urlRequest))
            }
        }
    }

    private func addAuthHeader(to request: URLRequest, token: String) -> URLRequest {
        var modifiedRequest = request
        modifiedRequest.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
        return modifiedRequest
    }

    private func shouldRefreshToken() -> Bool {
        // Implement token expiration logic
        return accessToken == nil || tokenRefreshService.isTokenExpired()
    }
}

enum AdapterError: Error {
    case selfDeallocated
    case tokenRefreshFailed
}

Request Signing Adapter

For APIs requiring request signing, create an adapter that adds cryptographic signatures:

import CryptoKit

class RequestSigningAdapter: RequestAdapter {
    private let secretKey: String
    private let keyId: String

    init(secretKey: String, keyId: String) {
        self.secretKey = secretKey
        self.keyId = keyId
    }

    func adapt(_ urlRequest: URLRequest, for session: Session, completion: @escaping (Result<URLRequest, Error>) -> Void) {
        var signedRequest = urlRequest

        let timestamp = String(Int(Date().timeIntervalSince1970))
        let nonce = UUID().uuidString

        // Create signature string
        let method = urlRequest.httpMethod ?? "GET"
        let path = urlRequest.url?.path ?? ""
        let query = urlRequest.url?.query ?? ""
        let body = urlRequest.httpBody.map { String(data: $0, encoding: .utf8) } ?? ""

        let signatureString = "\(method)\n\(path)\n\(query)\n\(body)\n\(timestamp)\n\(nonce)"

        // Generate HMAC signature
        if let signature = generateHMACSignature(for: signatureString) {
            signedRequest.setValue(keyId, forHTTPHeaderField: "X-API-Key-ID")
            signedRequest.setValue(timestamp, forHTTPHeaderField: "X-Timestamp")
            signedRequest.setValue(nonce, forHTTPHeaderField: "X-Nonce")
            signedRequest.setValue(signature, forHTTPHeaderField: "X-Signature")
        }

        completion(.success(signedRequest))
    }

    private func generateHMACSignature(for string: String) -> String? {
        guard let keyData = secretKey.data(using: .utf8),
              let messageData = string.data(using: .utf8) else {
            return nil
        }

        let key = SymmetricKey(data: keyData)
        let signature = HMAC<SHA256>.authenticationCode(for: messageData, using: key)
        return Data(signature).base64EncodedString()
    }
}

Implementing Request Retriers

Custom retriers handle failed requests and implement retry logic. Here's an advanced implementation:

class CustomRequestRetrier: RequestRetrier {
    private let maxRetryCount: Int
    private let retryDelay: TimeInterval
    private var retryQueue = DispatchQueue(label: "retrier", qos: .utility)

    init(maxRetryCount: Int = 3, retryDelay: TimeInterval = 1.0) {
        self.maxRetryCount = maxRetryCount
        self.retryDelay = retryDelay
    }

    func retry(_ request: Request, for session: Session, dueTo error: Error, completion: @escaping (RetryResult) -> Void) {
        retryQueue.async { [weak self] in
            guard let self = self else {
                completion(.doNotRetry)
                return
            }

            guard request.retryCount < self.maxRetryCount else {
                completion(.doNotRetry)
                return
            }

            // Determine if retry is appropriate based on error type
            if self.shouldRetry(for: error, request: request) {
                let delay = self.calculateRetryDelay(for: request.retryCount)
                completion(.retryWithDelay(delay))
            } else {
                completion(.doNotRetry)
            }
        }
    }

    private func shouldRetry(for error: Error, request: Request) -> Bool {
        // Check for specific error conditions
        if let afError = error as? AFError {
            switch afError {
            case .sessionTaskFailed(let sessionError):
                return isRetryableNetworkError(sessionError)
            case .responseValidationFailed:
                return false
            default:
                return false
            }
        }

        // Check HTTP status codes
        if let response = request.response,
           let statusCode = response.response?.statusCode {
            return isRetryableStatusCode(statusCode)
        }

        return false
    }

    private func isRetryableNetworkError(_ error: Error) -> Bool {
        let nsError = error as NSError

        // Network connectivity errors
        let retryableCodes = [
            NSURLErrorTimedOut,
            NSURLErrorCannotConnectToHost,
            NSURLErrorNetworkConnectionLost,
            NSURLErrorDNSLookupFailed,
            NSURLErrorNotConnectedToInternet
        ]

        return retryableCodes.contains(nsError.code)
    }

    private func isRetryableStatusCode(_ statusCode: Int) -> Bool {
        // Retry on server errors and rate limiting
        return [429, 500, 502, 503, 504].contains(statusCode)
    }

    private func calculateRetryDelay(for retryCount: Int) -> TimeInterval {
        // Exponential backoff with jitter
        let baseDelay = retryDelay * pow(2.0, Double(retryCount))
        let jitter = Double.random(in: 0...0.1) * baseDelay
        return baseDelay + jitter
    }
}

Configuring Session with Custom Adapters

Once you've created your custom adapters, configure your Alamofire session:

class NetworkManager {
    private let session: Session
    private let tokenService = TokenRefreshService()

    init(apiKey: String, baseURL: String) {
        // Create custom adapters
        let requestAdapter = CustomRequestAdapter(apiKey: apiKey, baseURL: baseURL)
        let tokenAdapter = TokenAuthAdapter(tokenRefreshService: tokenService)
        let signingAdapter = RequestSigningAdapter(secretKey: "your-secret-key", keyId: "your-key-id")
        let retrier = CustomRequestRetrier(maxRetryCount: 3, retryDelay: 1.0)

        // Combine multiple adapters
        let combinedAdapter = CombinedRequestAdapter(adapters: [
            requestAdapter,
            tokenAdapter,
            signingAdapter
        ])

        // Configure session
        let configuration = URLSessionConfiguration.default
        configuration.timeoutIntervalForRequest = 30
        configuration.timeoutIntervalForResource = 60

        self.session = Session(
            configuration: configuration,
            requestAdapter: combinedAdapter,
            requestRetrier: retrier
        )
    }

    func makeRequest<T: Codable>(
        _ url: String,
        method: HTTPMethod = .get,
        parameters: Parameters? = nil,
        encoding: ParameterEncoding = URLEncoding.default
    ) -> DataRequest {
        return session.request(
            url,
            method: method,
            parameters: parameters,
            encoding: encoding
        )
        .validate()
        .responseDecodable(of: T.self) { response in
            switch response.result {
            case .success(let data):
                print("Request succeeded: \(data)")
            case .failure(let error):
                print("Request failed: \(error)")
            }
        }
    }
}

Combining Multiple Adapters

Create a combined adapter to use multiple adapters together:

class CombinedRequestAdapter: RequestAdapter {
    private let adapters: [RequestAdapter]

    init(adapters: [RequestAdapter]) {
        self.adapters = adapters
    }

    func adapt(_ urlRequest: URLRequest, for session: Session, completion: @escaping (Result<URLRequest, Error>) -> Void) {
        processAdapters(urlRequest, adapters: adapters, session: session, completion: completion)
    }

    private func processAdapters(
        _ urlRequest: URLRequest,
        adapters: [RequestAdapter],
        session: Session,
        completion: @escaping (Result<URLRequest, Error>) -> Void
    ) {
        guard let firstAdapter = adapters.first else {
            completion(.success(urlRequest))
            return
        }

        let remainingAdapters = Array(adapters.dropFirst())

        firstAdapter.adapt(urlRequest, for: session) { [weak self] result in
            switch result {
            case .success(let adaptedRequest):
                if remainingAdapters.isEmpty {
                    completion(.success(adaptedRequest))
                } else {
                    self?.processAdapters(adaptedRequest, adapters: remainingAdapters, session: session, completion: completion)
                }
            case .failure(let error):
                completion(.failure(error))
            }
        }
    }
}

Advanced Use Cases

Caching Adapter

Implement request-level caching for improved performance:

class CachingRequestAdapter: RequestAdapter {
    private let cache = URLCache(
        memoryCapacity: 50 * 1024 * 1024, // 50MB
        diskCapacity: 100 * 1024 * 1024,  // 100MB
        diskPath: "alamofire_cache"
    )

    func adapt(_ urlRequest: URLRequest, for session: Session, completion: @escaping (Result<URLRequest, Error>) -> Void) {
        var cachingRequest = urlRequest

        // Set caching policy based on request type
        if urlRequest.httpMethod == "GET" {
            cachingRequest.cachePolicy = .returnCacheDataElseLoad
        } else {
            cachingRequest.cachePolicy = .reloadIgnoringLocalCacheData
        }

        completion(.success(cachingRequest))
    }
}

Logging and Analytics Adapter

Add comprehensive logging for debugging and analytics:

class LoggingRequestAdapter: RequestAdapter {
    private let logger: Logger

    init(logger: Logger = Logger()) {
        self.logger = logger
    }

    func adapt(_ urlRequest: URLRequest, for session: Session, completion: @escaping (Result<URLRequest, Error>) -> Void) {
        // Log request details
        logger.info("🚀 Outgoing Request:")
        logger.info("URL: \(urlRequest.url?.absoluteString ?? "Unknown")")
        logger.info("Method: \(urlRequest.httpMethod ?? "Unknown")")
        logger.info("Headers: \(urlRequest.allHTTPHeaderFields ?? [:])")

        if let body = urlRequest.httpBody,
           let bodyString = String(data: body, encoding: .utf8) {
            logger.info("Body: \(bodyString)")
        }

        // Add request tracking header
        var trackedRequest = urlRequest
        trackedRequest.setValue(UUID().uuidString, forHTTPHeaderField: "X-Request-ID")

        completion(.success(trackedRequest))
    }
}

Best Practices and Considerations

Error Handling

Always implement proper error handling in your adapters:

  • Use appropriate error types for different failure scenarios
  • Provide meaningful error messages for debugging
  • Consider network conditions and handle them gracefully

Performance Optimization

  • Keep adapter logic lightweight to avoid request delays
  • Use appropriate dispatch queues for async operations
  • Implement caching where beneficial

Security Considerations

When implementing custom network adapters, especially for web scraping applications, consider security aspects similar to handling browser sessions in Puppeteer where session management is crucial. Additionally, when dealing with dynamic content that requires JavaScript execution, you might need to integrate with tools that can handle such scenarios, much like handling AJAX requests using Puppeteer.

  • Never log sensitive information like API keys or tokens
  • Implement proper certificate validation
  • Use secure protocols (HTTPS) for all communications
  • Validate and sanitize all input parameters

Testing Custom Adapters

import XCTest
@testable import YourApp

class CustomRequestAdapterTests: XCTestCase {
    var adapter: CustomRequestAdapter!

    override func setUp() {
        super.setUp()
        adapter = CustomRequestAdapter(apiKey: "test-key", baseURL: "https://api.example.com")
    }

    func testAdapterAddsAuthorizationHeader() {
        let expectation = XCTestExpectation(description: "Adapter completion")
        let originalRequest = URLRequest(url: URL(string: "https://example.com/test")!)

        adapter.adapt(originalRequest, for: Session.default) { result in
            switch result {
            case .success(let adaptedRequest):
                XCTAssertEqual(
                    adaptedRequest.value(forHTTPHeaderField: "Authorization"),
                    "Bearer test-key"
                )
                expectation.fulfill()
            case .failure:
                XCTFail("Adapter should not fail for valid request")
            }
        }

        wait(for: [expectation], timeout: 1.0)
    }
}

Conclusion

Custom network adapters in Alamofire provide powerful capabilities for handling complex HTTP scenarios. By implementing request adapters and retriers, you can add authentication, request signing, caching, logging, and sophisticated retry logic to your networking layer. The key is to keep adapters focused, testable, and performant while handling errors gracefully.

Remember to thoroughly test your custom adapters and consider the security implications of any modifications you make to requests. With proper implementation, custom network adapters can significantly enhance your application's networking capabilities and provide better control over HTTP communications.

Try WebScraping.AI for Your Web Scraping Needs

Looking for a powerful web scraping solution? WebScraping.AI provides an LLM-powered API that combines Chromium JavaScript rendering with rotating proxies for reliable data extraction.

Key Features:

  • AI-powered extraction: Ask questions about web pages or extract structured data fields
  • JavaScript rendering: Full Chromium browser support for dynamic content
  • Rotating proxies: Datacenter and residential proxies from multiple countries
  • Easy integration: Simple REST API with SDKs for Python, Ruby, PHP, and more
  • Reliable & scalable: Built for developers who need consistent results

Getting Started:

Get page content with AI analysis:

curl "https://api.webscraping.ai/ai/question?url=https://example.com&question=What is the main topic?&api_key=YOUR_API_KEY"

Extract structured data:

curl "https://api.webscraping.ai/ai/fields?url=https://example.com&fields[title]=Page title&fields[price]=Product price&api_key=YOUR_API_KEY"

Try in request builder

Related Questions

Get Started Now

WebScraping.AI provides rotating proxies, Chromium rendering and built-in HTML parser for web scraping
Icon