cat swift/errori-http-decoding-mapping.md
Errori HTTP, decoding e mapping degli errori
Una buona API Swift non restituisce errori generici: distingue rete, status code, decoding, mapping e cancellazione.
Nel networking reale, “errore” può significare molte cose:
- URL invalido;
- connessione assente;
- timeout;
- risposta non HTTP;
- status code 404;
- status code 500;
- JSON non decodificabile;
- JSON valido ma dominio invalido;
- cancellazione.
Se trasformi tutto in Error, perdi diagnostica.
APIError
enum APIError: Error, Sendable {
case invalidURL
case invalidResponse
case statusCode(Int, data: Data)
case remote(code: String, message: String)
case transport(Error)
case decoding(Error)
case mapping(Error)
}
Questo errore distingue fasi diverse.
Status code
guard (200..<300).contains(http.statusCode) else {
throw APIError.statusCode(http.statusCode, data: data)
}
Mantieni data se la API manda un payload errore utile.
Error response DTO
struct ErrorResponseDTO: Decodable {
let code: String
let message: String
}
Puoi provare a decodificarlo:
if let apiError = try? decoder.decode(ErrorResponseDTO.self, from: data) {
throw APIError.remote(code: apiError.code, message: apiError.message)
}
Questo conserva l’informazione prodotta dal server senza fingere che sia un errore di decoding.
Decoding error
do {
return try decoder.decode(T.self, from: data)
} catch {
throw APIError.decoding(error)
}
Non sostituire subito con “impossibile caricare”. In fase di debug, DecodingError contiene il coding path e il motivo.
Mapping error
Se decodifichi DTO e poi converti:
func product(id: String) async throws -> Product {
let dto: ProductDTO = try await send(Endpoint(path: "products/\(id)"))
do {
return try dto.toDomain()
} catch {
throw APIError.mapping(error)
}
}
Decoding e mapping sono diversi:
- decoding: il JSON non corrisponde al DTO;
- mapping: il DTO non produce un dominio valido.
Cancellazione
Non trasformare CancellationError in APIError.
do {
return try await operation()
} catch is CancellationError {
throw CancellationError()
} catch {
throw APIError.transport(error)
}
Se aggiungi:
case transport(Error)
mantieni separata la cancellazione.
Mapping per status code
Non tutti gli status code hanno lo stesso significato.
func validate(_ response: HTTPURLResponse, data: Data) throws {
switch response.statusCode {
case 200..<300:
return
case 401:
throw APIError.remote(code: "unauthorized", message: "Accesso non autorizzato.")
case 404:
throw APIError.statusCode(404, data: data)
case 429:
throw APIError.remote(code: "rate_limited", message: "Troppe richieste.")
default:
throw APIError.statusCode(response.statusCode, data: data)
}
}
Questa funzione è piccola, testabile e impedisce di spargere if statusCode == ... in tutto il client.
Errori retryable
Il modello errori può aiutare il retry:
extension APIError {
var isRetryable: Bool {
switch self {
case .statusCode(let code, _):
return (500..<600).contains(code)
case .transport:
return true
case .invalidURL, .invalidResponse, .remote, .decoding, .mapping:
return false
}
}
}
Non fare retry su errori deterministici come mapping o decoding. Ripetere la stessa richiesta non trasformerà un prezzo negativo in un prezzo valido.
User-facing error
Al confine UI puoi mappare:
extension APIError {
var userMessage: String {
switch self {
case .invalidURL:
return "Richiesta non valida."
case .invalidResponse:
return "Risposta non valida dal server."
case .statusCode(404, _):
return "Elemento non trovato."
case .statusCode:
return "Errore del server."
case .decoding, .mapping:
return "I dati ricevuti non sono validi."
case .transport:
return "Controlla la connessione e riprova."
case .remote(_, let message):
return message
}
}
}
Non usare messaggi tecnici direttamente nella UI.
Checklist
Un buon modello errori distingue:
- trasporto;
- risposta non valida;
- status code;
- errore remoto;
- decoding;
- mapping;
- cancellazione.
E mantiene:
- dettagli per debug;
- messaggi adatti all’utente;
- errori tipizzati per test.
Test consigliati
Per APIError, scrivi test piccoli:
@Test
func statusCode500ERetryable() {
#expect(APIError.statusCode(500, data: Data()).isRetryable)
}
@Test
func decodingNonERetryable() {
#expect(APIError.decoding(DummyError()).isRetryable == false)
}
E test sul messaggio utente:
@Test
func remoteErrorUsaMessaggioServer() {
let error = APIError.remote(code: "quota", message: "Quota superata.")
#expect(error.userMessage == "Quota superata.")
}
Il modello errori diventa parte del contratto del client.
Esercizio
Estendi APIError con:
transport(Error);remote(code:message:);statusCode;decoding;mapping.
Poi scrivi una funzione userMessage e testala su 404, 500, decoding e remote error.