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
Always validate responses: Use Alamofire's validation features to ensure your app handles unexpected status codes gracefully.
Implement proper error handling: Create custom error types that map to specific status codes for better error reporting.
Handle authentication properly: Implement token refresh logic for 401 responses to maintain seamless user experience.
Use appropriate retry strategies: Implement exponential backoff for server errors and respect rate limiting headers.
Log status codes: Always log HTTP status codes for debugging and monitoring purposes.
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.