Table of contents

How do I handle different HTTP status codes with Alamofire?

HTTP status codes are crucial indicators of how your network requests are processed by servers. When working with Alamofire, Swift's most popular networking library, proper handling of these status codes ensures robust error handling and better user experience. This comprehensive guide covers everything you need to know about managing different HTTP status codes in your iOS applications.

Understanding HTTP Status Codes

HTTP status codes are three-digit numbers that indicate the outcome of an HTTP request. They're grouped into five categories:

  • 1xx (Informational): Request received, continuing process
  • 2xx (Successful): Request successfully received, understood, and accepted
  • 3xx (Redirection): Further action needed to complete the request
  • 4xx (Client Error): Request contains bad syntax or cannot be fulfilled
  • 5xx (Server Error): Server failed to fulfill an apparently valid request

Default Status Code Validation in Alamofire

By default, Alamofire considers HTTP status codes in the 200-299 range as successful. Any response outside this range will trigger an error unless you explicitly handle it.

import Alamofire

AF.request("https://api.example.com/data")
    .response { response in
        switch response.result {
        case .success(let data):
            // Handle successful response (2xx status codes)
            print("Success: \(String(data: data ?? Data(), encoding: .utf8) ?? "")")
        case .failure(let error):
            // Handle error (including 4xx and 5xx status codes)
            print("Error: \(error)")
        }
    }

Custom Status Code Validation

You can customize which status codes should be considered successful using the validate() method:

// Accept only specific status codes
AF.request("https://api.example.com/data")
    .validate(statusCode: 200..<300)
    .response { response in
        // Handle response
    }

// Accept multiple status code ranges
AF.request("https://api.example.com/data")
    .validate(statusCode: [200..<300, 400..<500])
    .response { response in
        // Handle response
    }

// Custom validation logic
AF.request("https://api.example.com/data")
    .validate { request, response, data in
        switch response.statusCode {
        case 200...299:
            return .success(Void())
        case 400:
            return .failure(APIError.badRequest)
        case 401:
            return .failure(APIError.unauthorized)
        case 404:
            return .failure(APIError.notFound)
        case 500...599:
            return .failure(APIError.serverError)
        default:
            return .failure(APIError.unknown)
        }
    }
    .response { response in
        // Handle validated response
    }

Handling Specific Status Codes

Accessing Response Status Code

You can directly access the HTTP status code from the response:

AF.request("https://api.example.com/data")
    .response { response in
        if let httpResponse = response.response {
            let statusCode = httpResponse.statusCode
            print("Status Code: \(statusCode)")

            switch statusCode {
            case 200:
                print("Success")
            case 201:
                print("Created")
            case 204:
                print("No Content")
            case 400:
                print("Bad Request")
            case 401:
                print("Unauthorized")
            case 403:
                print("Forbidden")
            case 404:
                print("Not Found")
            case 429:
                print("Too Many Requests")
            case 500:
                print("Internal Server Error")
            case 503:
                print("Service Unavailable")
            default:
                print("Other status code: \(statusCode)")
            }
        }
    }

Creating Custom Error Types

Define custom error types to handle different status codes systematically:

enum APIError: Error {
    case badRequest(String)
    case unauthorized
    case forbidden
    case notFound
    case tooManyRequests(retryAfter: Int?)
    case serverError(Int)
    case unknown(Int)

    var localizedDescription: String {
        switch self {
        case .badRequest(let message):
            return "Bad Request: \(message)"
        case .unauthorized:
            return "Unauthorized access"
        case .forbidden:
            return "Access forbidden"
        case .notFound:
            return "Resource not found"
        case .tooManyRequests(let retryAfter):
            let retry = retryAfter != nil ? " Retry after \(retryAfter!) seconds." : ""
            return "Too many requests.\(retry)"
        case .serverError(let code):
            return "Server error: \(code)"
        case .unknown(let code):
            return "Unknown error: \(code)"
        }
    }
}

extension AF.DataResponse {
    func mapError() -> APIError? {
        guard let statusCode = response?.statusCode else { return nil }

        switch statusCode {
        case 400:
            // Parse error message from response body if available
            if let data = data,
               let errorResponse = try? JSONDecoder().decode(ErrorResponse.self, from: data) {
                return .badRequest(errorResponse.message)
            }
            return .badRequest("Invalid request")
        case 401:
            return .unauthorized
        case 403:
            return .forbidden
        case 404:
            return .notFound
        case 429:
            let retryAfter = response?.value(forHTTPHeaderField: "Retry-After").flatMap(Int.init)
            return .tooManyRequests(retryAfter: retryAfter)
        case 500...599:
            return .serverError(statusCode)
        default:
            return .unknown(statusCode)
        }
    }
}

Advanced Status Code Handling

Retry Logic for Specific Status Codes

Implement retry logic for temporary failures:

func requestWithRetry(url: String, maxRetries: Int = 3) {
    performRequest(url: url, attempt: 1, maxRetries: maxRetries)
}

private func performRequest(url: String, attempt: Int, maxRetries: Int) {
    AF.request(url)
        .response { response in
            if let httpResponse = response.response {
                switch httpResponse.statusCode {
                case 200...299:
                    // Success - handle response
                    self.handleSuccess(response)
                case 429, 500...599:
                    // Retry for rate limiting or server errors
                    if attempt <= maxRetries {
                        let delay = Double(attempt * attempt) // Exponential backoff
                        DispatchQueue.main.asyncAfter(deadline: .now() + delay) {
                            self.performRequest(url: url, attempt: attempt + 1, maxRetries: maxRetries)
                        }
                    } else {
                        self.handleError(APIError.serverError(httpResponse.statusCode))
                    }
                case 401:
                    // Handle authentication error
                    self.handleAuthenticationError()
                default:
                    // Handle other errors
                    self.handleError(APIError.unknown(httpResponse.statusCode))
                }
            }
        }
}

Intercepting Responses Globally

Use Alamofire's interceptor system to handle status codes globally:

class StatusCodeInterceptor: RequestInterceptor {
    func retry(_ request: Request, for session: Session, dueTo error: Error, completion: @escaping (RetryResult) -> Void) {
        guard let response = request.task?.response as? HTTPURLResponse else {
            completion(.doNotRetry)
            return
        }

        switch response.statusCode {
        case 429:
            // Rate limited - retry with delay
            completion(.retryWithDelay(2.0))
        case 500...599:
            // Server error - retry up to 3 times
            if request.retryCount < 3 {
                completion(.retryWithDelay(1.0))
            } else {
                completion(.doNotRetry)
            }
        default:
            completion(.doNotRetry)
        }
    }
}

// Use the interceptor
let session = Session(interceptor: StatusCodeInterceptor())
session.request("https://api.example.com/data")
    .response { response in
        // Handle response
    }

Real-World Examples

Handling API Authentication

class APIManager {
    static let shared = APIManager()

    func makeAuthenticatedRequest<T: Codable>(
        _ type: T.Type,
        url: String,
        completion: @escaping (Result<T, APIError>) -> Void
    ) {
        AF.request(url, headers: getAuthHeaders())
            .responseDecodable(of: type) { response in
                if let httpResponse = response.response {
                    switch httpResponse.statusCode {
                    case 200...299:
                        if let value = response.value {
                            completion(.success(value))
                        }
                    case 401:
                        // Token expired - refresh and retry
                        self.refreshToken { success in
                            if success {
                                self.makeAuthenticatedRequest(type, url: url, completion: completion)
                            } else {
                                completion(.failure(.unauthorized))
                            }
                        }
                    case 403:
                        completion(.failure(.forbidden))
                    case 404:
                        completion(.failure(.notFound))
                    default:
                        completion(.failure(.unknown(httpResponse.statusCode)))
                    }
                }
            }
    }

    private func getAuthHeaders() -> HTTPHeaders {
        return ["Authorization": "Bearer \(getAccessToken())"]
    }

    private func refreshToken(completion: @escaping (Bool) -> Void) {
        // Implement token refresh logic
    }
}

Handling Pagination with Status Codes

class PaginationManager {
    func loadNextPage(url: String, completion: @escaping (Result<[DataModel], APIError>) -> Void) {
        AF.request(url)
            .responseDecodable(of: PaginatedResponse<DataModel>.self) { response in
                if let httpResponse = response.response {
                    switch httpResponse.statusCode {
                    case 200:
                        if let paginatedData = response.value {
                            completion(.success(paginatedData.results))
                        }
                    case 204:
                        // No more content - end of pagination
                        completion(.success([]))
                    case 400:
                        completion(.failure(.badRequest("Invalid pagination parameters")))
                    case 429:
                        // Rate limited - implement backoff strategy
                        let retryAfter = httpResponse.value(forHTTPHeaderField: "Retry-After").flatMap(Int.init) ?? 5
                        DispatchQueue.main.asyncAfter(deadline: .now() + Double(retryAfter)) {
                            self.loadNextPage(url: url, completion: completion)
                        }
                    default:
                        completion(.failure(.unknown(httpResponse.statusCode)))
                    }
                }
            }
    }
}

Testing Status Code Handling

When testing your status code handling logic, use Alamofire's testing capabilities:

import XCTest
import Alamofire

class StatusCodeHandlingTests: XCTestCase {
    func testSuccessStatusCode() {
        let expectation = XCTestExpectation(description: "Success response")

        AF.request("https://httpbin.org/status/200")
            .response { response in
                XCTAssertEqual(response.response?.statusCode, 200)
                expectation.fulfill()
            }

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

    func testErrorStatusCode() {
        let expectation = XCTestExpectation(description: "Error response")

        AF.request("https://httpbin.org/status/404")
            .response { response in
                XCTAssertEqual(response.response?.statusCode, 404)
                expectation.fulfill()
            }

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

Best Practices

  1. Always validate responses: Use Alamofire's validation features to ensure your app handles unexpected status codes gracefully.

  2. Implement proper error handling: Create custom error types that map to specific status codes for better error reporting.

  3. Handle authentication properly: Implement token refresh logic for 401 responses to maintain seamless user experience.

  4. Use appropriate retry strategies: Implement exponential backoff for server errors and respect rate limiting headers.

  5. Log status codes: Always log HTTP status codes for debugging and monitoring purposes.

  6. Handle edge cases: Consider less common status codes like 204 (No Content), 409 (Conflict), and 422 (Unprocessable Entity).

When building iOS applications that interact with web services, similar principles apply to handling errors in Puppeteer for web scraping scenarios, where proper error handling and retry logic are equally important.

Conclusion

Proper HTTP status code handling in Alamofire is essential for building robust iOS applications. By implementing custom validation, creating appropriate error types, and using retry strategies, you can ensure your app gracefully handles various server responses. Remember to always test your status code handling logic and implement appropriate logging for debugging purposes.

The key to successful status code handling is understanding what each code means in your application's context and implementing appropriate responses for each scenario. Whether you're dealing with authentication errors, rate limiting, or server failures, Alamofire provides the tools necessary to handle these situations elegantly.

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