cat swift/urlsession-richiesta-http-robusta.md

Lezione 51 55 min

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