How do I implement custom URLRequest modifications with Alamofire?
Alamofire provides several powerful mechanisms for implementing custom URLRequest modifications, allowing you to intercept, modify, and enhance HTTP requests before they're sent. This is essential for implementing authentication, custom headers, request logging, and other cross-cutting concerns in your iOS applications.
Understanding Alamofire's Request Modification System
Alamofire offers multiple approaches for customizing URLRequests:
- RequestAdapter - Modifies requests before they're sent
- RequestInterceptor - Combines adapter and retry functionality
- RequestModifier closures - Simple inline modifications
- Custom Session configurations - Global request modifications
Using RequestAdapter for Custom Modifications
The RequestAdapter
protocol is the most common way to implement custom URLRequest modifications:
import Alamofire
import Foundation
class CustomRequestAdapter: RequestAdapter {
private let apiKey: String
init(apiKey: String) {
self.apiKey = apiKey
}
func adapt(_ urlRequest: URLRequest, for session: Session, completion: @escaping (Result<URLRequest, Error>) -> Void) {
var modifiedRequest = urlRequest
// Add custom headers
modifiedRequest.setValue("application/json", forHTTPHeaderField: "Content-Type")
modifiedRequest.setValue("Bearer \(apiKey)", forHTTPHeaderField: "Authorization")
modifiedRequest.setValue("MyApp/1.0", forHTTPHeaderField: "User-Agent")
// Add timestamp header
let timestamp = String(Date().timeIntervalSince1970)
modifiedRequest.setValue(timestamp, forHTTPHeaderField: "X-Timestamp")
// Modify timeout
modifiedRequest.timeoutInterval = 30
completion(.success(modifiedRequest))
}
}
// Usage with Session
let adapter = CustomRequestAdapter(apiKey: "your-api-key")
let session = Session(interceptor: adapter)
session.request("https://api.example.com/data")
.responseJSON { response in
print(response)
}
Implementing RequestInterceptor for Advanced Scenarios
For more complex scenarios that require both request adaptation and retry logic, use RequestInterceptor
:
class AuthenticationInterceptor: RequestInterceptor {
private var accessToken: String?
private let refreshToken: String
private let tokenRefreshURL = "https://api.example.com/auth/refresh"
init(accessToken: String?, refreshToken: String) {
self.accessToken = accessToken
self.refreshToken = refreshToken
}
// MARK: - RequestAdapter
func adapt(_ urlRequest: URLRequest, for session: Session, completion: @escaping (Result<URLRequest, Error>) -> Void) {
var modifiedRequest = urlRequest
// Add authentication headers
if let token = accessToken {
modifiedRequest.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
}
// Add custom tracking headers
modifiedRequest.setValue(UUID().uuidString, forHTTPHeaderField: "X-Request-ID")
modifiedRequest.setValue("iOS", forHTTPHeaderField: "X-Platform")
completion(.success(modifiedRequest))
}
// MARK: - RequestRetrier
func retry(_ request: Request, for session: Session, dueTo error: Error, completion: @escaping (RetryResult) -> Void) {
guard let response = request.task?.response as? HTTPURLResponse,
response.statusCode == 401 else {
completion(.doNotRetry)
return
}
// Attempt to refresh token and retry
refreshAccessToken { [weak self] success in
if success {
completion(.retry)
} else {
completion(.doNotRetry)
}
}
}
private func refreshAccessToken(completion: @escaping (Bool) -> Void) {
let parameters = ["refresh_token": refreshToken]
AF.request(tokenRefreshURL, method: .post, parameters: parameters)
.responseJSON { [weak self] response in
if let json = response.value as? [String: Any],
let newToken = json["access_token"] as? String {
self?.accessToken = newToken
completion(true)
} else {
completion(false)
}
}
}
}
Using RequestModifier for Simple Modifications
For simple, one-off modifications, you can use the requestModifier
parameter:
let session = AF
session.request("https://api.example.com/data") { urlRequest in
// Modify the URLRequest inline
urlRequest.setValue("custom-value", forHTTPHeaderField: "X-Custom-Header")
urlRequest.setValue("gzip, deflate", forHTTPHeaderField: "Accept-Encoding")
urlRequest.cachePolicy = .reloadIgnoringLocalCacheData
urlRequest.timeoutInterval = 15
}
.responseJSON { response in
print(response)
}
Creating Specialized Request Adapters
API Key Authentication Adapter
class APIKeyAdapter: RequestAdapter {
private let apiKey: String
private let keyParameter: String
init(apiKey: String, keyParameter: String = "api_key") {
self.apiKey = apiKey
self.keyParameter = keyParameter
}
func adapt(_ urlRequest: URLRequest, for session: Session, completion: @escaping (Result<URLRequest, Error>) -> Void) {
guard let url = urlRequest.url,
var urlComponents = URLComponents(url: url, resolvingAgainstBaseURL: false) else {
completion(.failure(AFError.invalidURL(url: urlRequest.url)))
return
}
var modifiedRequest = urlRequest
// Add API key as query parameter
var queryItems = urlComponents.queryItems ?? []
queryItems.append(URLQueryItem(name: keyParameter, value: apiKey))
urlComponents.queryItems = queryItems
modifiedRequest.url = urlComponents.url
completion(.success(modifiedRequest))
}
}
Request Signing Adapter
import CommonCrypto
class RequestSigningAdapter: RequestAdapter {
private let secretKey: String
init(secretKey: String) {
self.secretKey = secretKey
}
func adapt(_ urlRequest: URLRequest, for session: Session, completion: @escaping (Result<URLRequest, Error>) -> Void) {
var modifiedRequest = urlRequest
let timestamp = String(Int(Date().timeIntervalSince1970))
let method = urlRequest.httpMethod ?? "GET"
let path = urlRequest.url?.path ?? ""
let body = urlRequest.httpBody.map { String(data: $0, encoding: .utf8) ?? "" } ?? ""
// Create signature string
let stringToSign = "\(method)\n\(path)\n\(timestamp)\n\(body)"
let signature = hmacSHA256(data: stringToSign, key: secretKey)
// Add signature headers
modifiedRequest.setValue(timestamp, forHTTPHeaderField: "X-Timestamp")
modifiedRequest.setValue(signature, forHTTPHeaderField: "X-Signature")
completion(.success(modifiedRequest))
}
private func hmacSHA256(data: String, key: String) -> String {
let keyData = key.data(using: .utf8)!
let messageData = data.data(using: .utf8)!
var result = [UInt8](repeating: 0, count: Int(CC_SHA256_DIGEST_LENGTH))
keyData.withUnsafeBytes { keyBytes in
messageData.withUnsafeBytes { messageBytes in
CCHmac(CCHmacAlgorithm(kCCHmacAlgSHA256),
keyBytes.baseAddress, keyData.count,
messageBytes.baseAddress, messageData.count,
&result)
}
}
return Data(result).base64EncodedString()
}
}
Combining Multiple Adapters
You can combine multiple request adapters using Interceptor
:
let authAdapter = CustomRequestAdapter(apiKey: "your-api-key")
let signingAdapter = RequestSigningAdapter(secretKey: "your-secret")
// Create a combined interceptor
let combinedInterceptor = Interceptor(adapters: [authAdapter, signingAdapter])
let session = Session(interceptor: combinedInterceptor)
Advanced URLRequest Modifications
Dynamic Header Injection
class DynamicHeaderAdapter: RequestAdapter {
private let headerProvider: () -> [String: String]
init(headerProvider: @escaping () -> [String: String]) {
self.headerProvider = headerProvider
}
func adapt(_ urlRequest: URLRequest, for session: Session, completion: @escaping (Result<URLRequest, Error>) -> Void) {
var modifiedRequest = urlRequest
// Apply dynamic headers
let headers = headerProvider()
for (key, value) in headers {
modifiedRequest.setValue(value, forHTTPHeaderField: key)
}
completion(.success(modifiedRequest))
}
}
// Usage with dynamic headers
let dynamicAdapter = DynamicHeaderAdapter {
return [
"X-Device-ID": UIDevice.current.identifierForVendor?.uuidString ?? "unknown",
"X-App-Version": Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "1.0",
"X-Locale": Locale.current.identifier
]
}
Request Body Transformation
class RequestBodyAdapter: RequestAdapter {
func adapt(_ urlRequest: URLRequest, for session: Session, completion: @escaping (Result<URLRequest, Error>) -> Void) {
var modifiedRequest = urlRequest
// Modify request body
if let originalBody = urlRequest.httpBody,
let bodyString = String(data: originalBody, encoding: .utf8),
var json = try? JSONSerialization.jsonObject(with: originalBody) as? [String: Any] {
// Add metadata to request body
json["metadata"] = [
"timestamp": Date().timeIntervalSince1970,
"client": "iOS-App",
"version": "1.0"
]
if let modifiedData = try? JSONSerialization.data(withJSONObject: json) {
modifiedRequest.httpBody = modifiedData
modifiedRequest.setValue(String(modifiedData.count), forHTTPHeaderField: "Content-Length")
}
}
completion(.success(modifiedRequest))
}
}
Error Handling and Debugging
When implementing custom URLRequest modifications, proper error handling is crucial:
class SafeRequestAdapter: RequestAdapter {
func adapt(_ urlRequest: URLRequest, for session: Session, completion: @escaping (Result<URLRequest, Error>) -> Void) {
do {
var modifiedRequest = urlRequest
// Validate URL
guard urlRequest.url != nil else {
completion(.failure(AFError.invalidURL(url: urlRequest.url)))
return
}
// Apply modifications with error handling
try applyCustomModifications(&modifiedRequest)
completion(.success(modifiedRequest))
} catch {
completion(.failure(error))
}
}
private func applyCustomModifications(_ request: inout URLRequest) throws {
// Your modification logic here
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
// Add debugging information in development
#if DEBUG
print("Modified request: \(request.debugDescription)")
#endif
}
}
Testing Custom Request Adapters
import XCTest
@testable import YourApp
class RequestAdapterTests: XCTestCase {
func testCustomRequestAdapter() {
let adapter = CustomRequestAdapter(apiKey: "test-key")
var request = URLRequest(url: URL(string: "https://api.example.com")!)
let expectation = self.expectation(description: "Adapter completion")
adapter.adapt(request, for: Session.default) { result in
switch result {
case .success(let modifiedRequest):
XCTAssertEqual(modifiedRequest.value(forHTTPHeaderField: "Authorization"), "Bearer test-key")
XCTAssertEqual(modifiedRequest.value(forHTTPHeaderField: "Content-Type"), "application/json")
case .failure(let error):
XCTFail("Adapter failed: \(error)")
}
expectation.fulfill()
}
waitForExpectations(timeout: 1.0)
}
}
Best Practices for URLRequest Modifications
- Keep adapters focused - Each adapter should handle a single concern
- Handle errors gracefully - Always provide proper error handling
- Avoid blocking operations - Request adapters should be fast and non-blocking
- Use dependency injection - Make adapters testable and configurable
- Log modifications - Add debugging information for development builds
- Validate modifications - Ensure your changes don't break the request
Conclusion
Custom URLRequest modifications with Alamofire provide a powerful way to implement cross-cutting concerns like authentication, logging, and request transformation. By using RequestAdapter, RequestInterceptor, and other modification patterns, you can create maintainable and reusable request customization logic that enhances your networking layer.
Whether you need simple header additions or complex request signing, Alamofire's modification system gives you the flexibility to handle any requirements while maintaining clean, testable code. Remember to handle errors properly and keep your adapters focused on single responsibilities for the best results.