tabbar
This commit is contained in:
@@ -7,8 +7,10 @@ enum NetworkError: Error, LocalizedError {
|
||||
case invalidURL
|
||||
case invalidResponse
|
||||
case httpError(Int, String?)
|
||||
case decodeError
|
||||
case rateLimited
|
||||
case unauthorized(String?)
|
||||
case decodeError(String?)
|
||||
case rateLimited(Int?)
|
||||
case transportError(String)
|
||||
|
||||
var errorDescription: String? {
|
||||
switch self {
|
||||
@@ -16,8 +18,20 @@ enum NetworkError: Error, LocalizedError {
|
||||
case .invalidResponse: return "Resposta invalida do servidor"
|
||||
case .httpError(let code, let message):
|
||||
return message ?? "Erro HTTP (\(code))"
|
||||
case .decodeError: return "Erro ao interpretar dados"
|
||||
case .rateLimited: return "Muitas requisicoes. Tente novamente."
|
||||
case .unauthorized(let message):
|
||||
return message ?? "Sessao expirada. Faca login novamente."
|
||||
case .decodeError(let payload):
|
||||
if let payload, payload.isEmpty == false {
|
||||
return "Erro ao interpretar dados: \(payload)"
|
||||
}
|
||||
return "Erro ao interpretar dados"
|
||||
case .rateLimited(let retryAfter):
|
||||
if let retryAfter {
|
||||
return "Muitas requisicoes. Tente novamente em \(retryAfter)s."
|
||||
}
|
||||
return "Muitas requisicoes. Tente novamente."
|
||||
case .transportError(let message):
|
||||
return "Erro de rede: \(message)"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -48,6 +62,8 @@ struct ApiRequest {
|
||||
final class ApiClient {
|
||||
private let session: URLSession
|
||||
private let tokenStore: TokenStore
|
||||
private let maxAttempts = 3
|
||||
private let baseBackoffNanoseconds: UInt64 = 300_000_000
|
||||
|
||||
init(session: URLSession = .shared, tokenStore: TokenStore = DefaultTokenStore()) {
|
||||
self.session = session
|
||||
@@ -69,27 +85,93 @@ final class ApiClient {
|
||||
urlRequest.setValue("Bearer \(jwt)", forHTTPHeaderField: "Authorization")
|
||||
}
|
||||
|
||||
let (data, response) = try await session.data(for: urlRequest)
|
||||
var attempt = 1
|
||||
while attempt <= maxAttempts {
|
||||
do {
|
||||
return try await perform(urlRequest, as: T.self)
|
||||
} catch let error as NetworkError {
|
||||
guard shouldRetry(error), attempt < maxAttempts else {
|
||||
throw error
|
||||
}
|
||||
try await Task.sleep(nanoseconds: backoff(for: attempt, error: error))
|
||||
attempt += 1
|
||||
} catch {
|
||||
let wrapped = NetworkError.transportError(error.localizedDescription)
|
||||
guard attempt < maxAttempts else { throw wrapped }
|
||||
try await Task.sleep(nanoseconds: backoff(for: attempt, error: wrapped))
|
||||
attempt += 1
|
||||
}
|
||||
}
|
||||
throw NetworkError.invalidResponse
|
||||
}
|
||||
|
||||
private func perform<T: Decodable>(_ request: URLRequest, as type: T.Type) async throws -> T {
|
||||
let (data, response): (Data, URLResponse)
|
||||
do {
|
||||
(data, response) = try await session.data(for: request)
|
||||
} catch {
|
||||
throw NetworkError.transportError(error.localizedDescription)
|
||||
}
|
||||
|
||||
guard let http = response as? HTTPURLResponse else {
|
||||
throw NetworkError.invalidResponse
|
||||
}
|
||||
|
||||
if http.statusCode == 429 {
|
||||
throw NetworkError.rateLimited
|
||||
let retryAfter = Int(http.value(forHTTPHeaderField: "Retry-After") ?? "")
|
||||
throw NetworkError.rateLimited(retryAfter)
|
||||
}
|
||||
|
||||
if http.statusCode == 401 || http.statusCode == 403 {
|
||||
throw NetworkError.unauthorized(serverMessage(from: data))
|
||||
}
|
||||
|
||||
if !(200...299).contains(http.statusCode) {
|
||||
let message = String(data: data, encoding: .utf8)
|
||||
throw NetworkError.httpError(http.statusCode, message)
|
||||
throw NetworkError.httpError(http.statusCode, serverMessage(from: data))
|
||||
}
|
||||
|
||||
do {
|
||||
return try JSONDecoder().decode(T.self, from: data)
|
||||
return try JSONDecoder().decode(type, from: data)
|
||||
} catch {
|
||||
throw NetworkError.decodeError
|
||||
throw NetworkError.decodeError(String(data: data, encoding: .utf8))
|
||||
}
|
||||
}
|
||||
|
||||
private func shouldRetry(_ error: NetworkError) -> Bool {
|
||||
switch error {
|
||||
case .rateLimited, .transportError:
|
||||
return true
|
||||
case .httpError(let statusCode, _):
|
||||
return statusCode >= 500
|
||||
case .invalidURL, .invalidResponse, .decodeError, .unauthorized:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
private func backoff(for attempt: Int, error: NetworkError) -> UInt64 {
|
||||
if case .rateLimited(let retryAfter) = error, let retryAfter {
|
||||
return UInt64(retryAfter) * 1_000_000_000
|
||||
}
|
||||
let multiplier = UInt64(max(1, attempt))
|
||||
return min(baseBackoffNanoseconds * multiplier, 2_000_000_000)
|
||||
}
|
||||
|
||||
private func serverMessage(from data: Data) -> String? {
|
||||
if let envelope = try? JSONDecoder().decode(ApiEnvelope<EmptyResult>.self, from: data) {
|
||||
return envelope.message
|
||||
}
|
||||
|
||||
if let object = try? JSONSerialization.jsonObject(with: data) as? [String: Any] {
|
||||
if let message = object["message"] as? String {
|
||||
return message
|
||||
}
|
||||
if let code = object["code"] as? String {
|
||||
return "Erro: \(code)"
|
||||
}
|
||||
}
|
||||
return String(data: data, encoding: .utf8)
|
||||
}
|
||||
|
||||
private func buildURL(path: String, query: [URLQueryItem]) throws -> URL {
|
||||
guard var components = URLComponents(url: ApiConfig.baseURL, resolvingAgainstBaseURL: false) else {
|
||||
throw NetworkError.invalidURL
|
||||
|
||||
Reference in New Issue
Block a user