cat swift/urlsession-richiesta-http-robusta.md
URLSession e richiesta HTTP robusta
URLSession con async/await rende il networking leggibile, ma serve validare URL, response, status code e body.
URLSession è la base del networking in Foundation. Con async/await, il codice diventa molto più lineare, ma resta tua responsabilità validare la risposta.
Una richiesta robusta non è solo:
let (data, _) = try await URLSession.shared.data(from: url)
Devi controllare:
- URL;
- metodo;
- header;
- status code;
- body;
- decoding;
- cancellazione.
Client base
struct APIClient: Sendable {
let baseURL: URL
let session: URLSession
let decoder: JSONDecoder
init(baseURL: URL, session: URLSession = .shared, decoder: JSONDecoder = JSONDecoder()) {
self.baseURL = baseURL
self.session = session
self.decoder = decoder
}
}
Il client è un value type. Le dipendenze sono iniettate.
Endpoint
struct Endpoint {
let path: String
let queryItems: [URLQueryItem]
init(path: String, queryItems: [URLQueryItem] = []) {
self.path = path
self.queryItems = queryItems
}
}
Separare endpoint evita stringhe sparse.
Costruire URL
func url(for endpoint: Endpoint) throws -> URL {
let url = baseURL.appending(path: endpoint.path)
guard var components = URLComponents(url: url, resolvingAgainstBaseURL: true) else {
throw APIError.invalidURL
}
components.queryItems = endpoint.queryItems.isEmpty ? nil : endpoint.queryItems
guard let finalURL = components.url else {
throw APIError.invalidURL
}
return finalURL
}
Non costruire URL concatenando stringhe se hai query param.
Request
func request(for endpoint: Endpoint) throws -> URLRequest {
var request = URLRequest(url: try url(for: endpoint))
request.httpMethod = "GET"
request.setValue("application/json", forHTTPHeaderField: "Accept")
return request
}
Anche se ora fai solo GET, incapsulare request prepara POST, auth e header.
Eseguire richiesta
func send<T: Decodable & Sendable>(_ endpoint: Endpoint, as type: T.Type = T.self) async throws -> T {
let request = try request(for: endpoint)
let (data, response) = try await session.data(for: request)
guard let http = response as? HTTPURLResponse else {
throw APIError.invalidResponse
}
guard (200..<300).contains(http.statusCode) else {
throw APIError.statusCode(http.statusCode, data: data)
}
do {
return try decoder.decode(T.self, from: data)
} catch {
throw APIError.decoding(error)
}
}
Nota: data(for:) usa URLRequest, più flessibile di data(from:).
API specifica
extension APIClient {
func products() async throws -> [ProductDTO] {
try await send(Endpoint(path: "products"))
}
func product(id: String) async throws -> ProductDTO {
try await send(Endpoint(path: "products/\(id)"))
}
}
Il resto dell’app non deve conoscere path e request.
Header e auth
Per token:
request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
Non mettere token come stringhe globali. Iniettarli da un TokenProvider o actor sarà più sicuro nei moduli architettura.
Checklist
Una richiesta robusta:
- costruisce URL con
URLComponents; - usa
URLRequest; - imposta header;
- valida
HTTPURLResponse; - controlla status code;
- preserva body errore se utile;
- incapsula decoding;
- non espone dettagli HTTP al dominio.
Esercizio
Crea APIClient con:
baseURL;session;decoder;Endpoint;send<T: Decodable & Sendable>.
Poi aggiungi:
func products() async throws -> [ProductDTO]
func product(id: String) async throws -> ProductDTO