Table of contents

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:

  1. RequestAdapter - Modifies requests before they're sent
  2. RequestInterceptor - Combines adapter and retry functionality
  3. RequestModifier closures - Simple inline modifications
  4. 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

  1. Keep adapters focused - Each adapter should handle a single concern
  2. Handle errors gracefully - Always provide proper error handling
  3. Avoid blocking operations - Request adapters should be fast and non-blocking
  4. Use dependency injection - Make adapters testable and configurable
  5. Log modifications - Add debugging information for development builds
  6. 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.

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