Richieste HTTP in Java
Java offre diversi strumenti per effettuare richieste HTTP, dalle API storiche incluse nel JDK fino a librerie di terze parti ampiamente adottate nell'ecosistema enterprise. In questo articolo analizziamo le principali soluzioni disponibili, illustrandone il funzionamento con esempi pratici e completi.
HttpURLConnection
HttpURLConnection è la classe storica del JDK per effettuare richieste HTTP. Fa parte del package java.net ed è disponibile fin dalle prime versioni di Java. Pur essendo verbosa rispetto alle API moderne, è ancora largamente utilizzata in ambienti dove non è possibile introdurre dipendenze esterne.
Il seguente esempio mostra come effettuare una richiesta GET e leggere la risposta:
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.URL;
public class HttpGetExample {
// Esegue una richiesta GET all'URL specificato e restituisce il corpo della risposta
public static String sendGetRequest(String targetUrl) throws Exception {
URL url = new URL(targetUrl);
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
// Imposta il metodo HTTP
connection.setRequestMethod("GET");
// Imposta gli header della richiesta
connection.setRequestProperty("Accept", "application/json");
connection.setRequestProperty("User-Agent", "Java-HttpClient/1.0");
// Legge il codice di stato della risposta
int statusCode = connection.getResponseCode();
// Legge il corpo della risposta riga per riga
BufferedReader reader = new BufferedReader(
new InputStreamReader(connection.getInputStream())
);
StringBuilder responseBody = new StringBuilder();
String line;
while ((line = reader.readLine()) != null) {
responseBody.append(line);
}
reader.close();
connection.disconnect();
return responseBody.toString();
}
public static void main(String[] args) throws Exception {
String response = sendGetRequest("https://jsonplaceholder.typicode.com/posts/1");
System.out.println(response);
}
}
Per inviare dati con una richiesta POST è necessario abilitare l'output sulla connessione tramite setDoOutput(true) e scrivere il payload sullo stream di uscita:
import java.io.DataOutputStream;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.URL;
import java.nio.charset.StandardCharsets;
public class HttpPostExample {
// Invia una richiesta POST con un corpo JSON
public static String sendPostRequest(String targetUrl, String jsonBody) throws Exception {
URL url = new URL(targetUrl);
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
// Configura la connessione per il metodo POST
connection.setRequestMethod("POST");
connection.setDoOutput(true);
// Imposta gli header necessari per JSON
connection.setRequestProperty("Content-Type", "application/json");
connection.setRequestProperty("Accept", "application/json");
// Scrive il payload sul flusso di output
byte[] payload = jsonBody.getBytes(StandardCharsets.UTF_8);
connection.setRequestProperty("Content-Length", String.valueOf(payload.length));
try (DataOutputStream outputStream = new DataOutputStream(connection.getOutputStream())) {
outputStream.write(payload);
outputStream.flush();
}
// Legge la risposta del server
BufferedReader reader = new BufferedReader(
new InputStreamReader(connection.getInputStream())
);
StringBuilder responseBody = new StringBuilder();
String line;
while ((line = reader.readLine()) != null) {
responseBody.append(line);
}
reader.close();
connection.disconnect();
return responseBody.toString();
}
public static void main(String[] args) throws Exception {
String json = "{\"title\":\"Test\",\"body\":\"Contenuto del post\",\"userId\":1}";
String response = sendPostRequest("https://jsonplaceholder.typicode.com/posts", json);
System.out.println(response);
}
}
HttpClient (Java 11+)
A partire da Java 11 è disponibile il nuovo HttpClient, introdotto nel package java.net.http. Questa API è moderna, immutabile, supporta HTTP/2 e le chiamate asincrone tramite CompletableFuture. Rappresenta lo standard consigliato per i progetti che non necessitano di compatibilità con versioni precedenti del JDK.
Vediamo prima una richiesta GET sincrona:
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
public class ModernHttpGetExample {
public static void main(String[] args) throws Exception {
// Crea un'istanza del client HTTP riutilizzabile
HttpClient client = HttpClient.newHttpClient();
// Costruisce la richiesta GET
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("https://jsonplaceholder.typicode.com/posts/1"))
.header("Accept", "application/json")
.GET()
.build();
// Invia la richiesta e riceve la risposta come stringa
HttpResponse<String> response = client.send(
request,
HttpResponse.BodyHandlers.ofString()
);
// Stampa il codice di stato e il corpo della risposta
System.out.println("Codice di stato: " + response.statusCode());
System.out.println("Corpo: " + response.body());
}
}
La versione asincrona della stessa richiesta utilizza sendAsync, che restituisce un CompletableFuture:
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.util.concurrent.CompletableFuture;
public class AsyncHttpGetExample {
public static void main(String[] args) throws Exception {
HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("https://jsonplaceholder.typicode.com/posts/1"))
.header("Accept", "application/json")
.GET()
.build();
// Avvia la richiesta in modo asincrono
CompletableFuture<HttpResponse<String>> futureResponse = client.sendAsync(
request,
HttpResponse.BodyHandlers.ofString()
);
// Elabora la risposta quando disponibile
futureResponse
.thenApply(HttpResponse::body)
.thenAccept(body -> System.out.println("Risposta ricevuta: " + body))
.join(); // Attende il completamento prima di terminare il programma
}
}
Una richiesta POST con HttpClient è significativamente più leggibile rispetto alla versione con HttpURLConnection:
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;
public class ModernHttpPostExample {
public static void main(String[] args) throws Exception {
HttpClient client = HttpClient.newHttpClient();
String jsonBody = "{\"title\":\"Nuovo post\",\"body\":\"Testo del post\",\"userId\":1}";
// Costruisce la richiesta POST con il corpo JSON
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("https://jsonplaceholder.typicode.com/posts"))
.header("Content-Type", "application/json")
.header("Accept", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(jsonBody, StandardCharsets.UTF_8))
.build();
HttpResponse<String> response = client.send(
request,
HttpResponse.BodyHandlers.ofString()
);
System.out.println("Codice di stato: " + response.statusCode());
System.out.println("Corpo: " + response.body());
}
}
Timeout e gestione degli errori
In un contesto di produzione è fondamentale configurare timeout appropriati e gestire correttamente i codici di errore HTTP. Il nuovo HttpClient permette di impostare sia un timeout globale sul client sia uno specifico per ogni singola richiesta:
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.time.Duration;
public class HttpWithTimeoutExample {
// Crea un client con timeout di connessione globale
private static final HttpClient client = HttpClient.newBuilder()
.connectTimeout(Duration.ofSeconds(5))
.build();
public static String fetchWithTimeout(String targetUrl) throws Exception {
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(targetUrl))
.timeout(Duration.ofSeconds(10)) // Timeout specifico per questa richiesta
.GET()
.build();
HttpResponse<String> response = client.send(
request,
HttpResponse.BodyHandlers.ofString()
);
// Verifica il codice di stato prima di restituire il corpo
int statusCode = response.statusCode();
if (statusCode >= 200 && statusCode < 300) {
return response.body();
} else if (statusCode >= 400 && statusCode < 500) {
throw new RuntimeException("Errore del client: " + statusCode);
} else if (statusCode >= 500) {
throw new RuntimeException("Errore del server: " + statusCode);
}
return response.body();
}
public static void main(String[] args) {
try {
String result = fetchWithTimeout("https://jsonplaceholder.typicode.com/posts/1");
System.out.println(result);
} catch (Exception e) {
System.err.println("Richiesta fallita: " + e.getMessage());
}
}
}
Richieste parallele con CompletableFuture
Un caso d'uso comune nelle applicazioni moderne è l'invio di più richieste HTTP in parallelo per ridurre il tempo complessivo di attesa. Il nuovo HttpClient si integra nativamente con CompletableFuture per questo scopo:
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.stream.Collectors;
public class ParallelRequestsExample {
private static final HttpClient client = HttpClient.newHttpClient();
// Invia richieste GET in parallelo a tutti gli URL forniti
public static List<String> fetchAll(List<String> urls) {
List<CompletableFuture<String>> futures = urls.stream()
.map(url -> {
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(url))
.GET()
.build();
// Ogni richiesta viene inviata in modo asincrono
return client
.sendAsync(request, HttpResponse.BodyHandlers.ofString())
.thenApply(HttpResponse::body);
})
.collect(Collectors.toList());
// Attende il completamento di tutte le richieste
CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join();
// Raccoglie e restituisce tutti i risultati
return futures.stream()
.map(CompletableFuture::join)
.collect(Collectors.toList());
}
public static void main(String[] args) {
List<String> urls = List.of(
"https://jsonplaceholder.typicode.com/posts/1",
"https://jsonplaceholder.typicode.com/posts/2",
"https://jsonplaceholder.typicode.com/posts/3"
);
List<String> results = fetchAll(urls);
results.forEach(body -> System.out.println(body + "\n---"));
}
}
Autenticazione HTTP Basic e Bearer Token
Molte API REST richiedono autenticazione. I due schemi più diffusi sono HTTP Basic Authentication e Bearer Token. Entrambi si implementano tramite l'header Authorization:
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.util.Base64;
public class HttpAuthExample {
private static final HttpClient client = HttpClient.newHttpClient();
// Richiesta con autenticazione Basic (username e password codificati in Base64)
public static String fetchWithBasicAuth(String targetUrl, String username, String password)
throws Exception {
String credentials = username + ":" + password;
String encodedCredentials = Base64.getEncoder()
.encodeToString(credentials.getBytes());
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(targetUrl))
.header("Authorization", "Basic " + encodedCredentials)
.GET()
.build();
return client.send(request, HttpResponse.BodyHandlers.ofString()).body();
}
// Richiesta con autenticazione Bearer Token (tipica delle API OAuth2)
public static String fetchWithBearerToken(String targetUrl, String token)
throws Exception {
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(targetUrl))
.header("Authorization", "Bearer " + token)
.GET()
.build();
return client.send(request, HttpResponse.BodyHandlers.ofString()).body();
}
public static void main(String[] args) throws Exception {
// Esempio con Bearer Token
String response = fetchWithBearerToken(
"https://api.example.com/protected-resource",
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
);
System.out.println(response);
}
}
Invio di form data e multipart
Alcune API accettano dati in formato application/x-www-form-urlencoded oppure multipart/form-data (tipicamente per l'upload di file). Con il nuovo HttpClient il primo caso è semplice, mentre per il multipart è necessario costruire il boundary manualmente o affidarsi a una libreria:
import java.net.URI;
import java.net.URLEncoder;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;
import java.util.Map;
import java.util.stream.Collectors;
public class FormDataExample {
private static final HttpClient client = HttpClient.newHttpClient();
// Codifica una mappa di parametri nel formato application/x-www-form-urlencoded
private static String encodeFormData(Map<String, String> params) {
return params.entrySet().stream()
.map(entry ->
URLEncoder.encode(entry.getKey(), StandardCharsets.UTF_8)
+ "="
+ URLEncoder.encode(entry.getValue(), StandardCharsets.UTF_8)
)
.collect(Collectors.joining("&"));
}
public static String sendFormPost(String targetUrl, Map<String, String> formParams)
throws Exception {
String formBody = encodeFormData(formParams);
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(targetUrl))
.header("Content-Type", "application/x-www-form-urlencoded")
.POST(HttpRequest.BodyPublishers.ofString(formBody))
.build();
return client.send(request, HttpResponse.BodyHandlers.ofString()).body();
}
public static void main(String[] args) throws Exception {
Map<String, String> fields = Map.of(
"username", "gabriele",
"password", "segreto123"
);
String response = sendFormPost("https://httpbin.org/post", fields);
System.out.println(response);
}
}
Utilizzo di OkHttp
OkHttp è una libreria open source sviluppata da Square, molto diffusa nell'ecosistema Java e Android. Offre un'API fluente, supporto trasparente per HTTP/2, connection pooling, retry automatico e interceptor per la personalizzazione delle richieste. Per includerla in un progetto Maven è sufficiente aggiungere la seguente dipendenza al file pom.xml:
<dependency>
<groupId>com.squareup.okhttp3</groupId>
<artifactId>okhttp</artifactId>
<version>4.12.0</version>
</dependency>
Ecco come effettuare una richiesta GET e una POST con OkHttp:
import okhttp3.MediaType;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;
public class OkHttpExample {
// Tipo di contenuto JSON riutilizzabile
private static final MediaType JSON_TYPE = MediaType.get("application/json");
// Client OkHttp condiviso (thread-safe, da riutilizzare)
private static final OkHttpClient client = new OkHttpClient();
// Invia una richiesta GET e restituisce il corpo della risposta
public static String get(String targetUrl) throws Exception {
Request request = new Request.Builder()
.url(targetUrl)
.addHeader("Accept", "application/json")
.build();
try (Response response = client.newCall(request).execute()) {
if (!response.isSuccessful()) {
throw new RuntimeException("Codice di stato inatteso: " + response.code());
}
return response.body().string();
}
}
// Invia una richiesta POST con un corpo JSON
public static String post(String targetUrl, String jsonBody) throws Exception {
RequestBody body = RequestBody.create(jsonBody, JSON_TYPE);
Request request = new Request.Builder()
.url(targetUrl)
.post(body)
.addHeader("Accept", "application/json")
.build();
try (Response response = client.newCall(request).execute()) {
if (!response.isSuccessful()) {
throw new RuntimeException("Codice di stato inatteso: " + response.code());
}
return response.body().string();
}
}
public static void main(String[] args) throws Exception {
// Esempio GET
String getResponse = get("https://jsonplaceholder.typicode.com/posts/1");
System.out.println("GET: " + getResponse);
// Esempio POST
String postResponse = post(
"https://jsonplaceholder.typicode.com/posts",
"{\"title\":\"Test\",\"body\":\"Testo\",\"userId\":1}"
);
System.out.println("POST: " + postResponse);
}
}
Un punto di forza di OkHttp è il sistema degli interceptor, che consente di aggiungere comportamenti trasversali come logging, autenticazione automatica e retry:
import okhttp3.Interceptor;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
import java.io.IOException;
import java.util.concurrent.TimeUnit;
public class OkHttpInterceptorExample {
// Interceptor per aggiungere automaticamente il token di autenticazione
static class AuthInterceptor implements Interceptor {
private final String token;
AuthInterceptor(String token) {
this.token = token;
}
@Override
public Response intercept(Chain chain) throws IOException {
// Aggiunge l'header Authorization a ogni richiesta
Request originalRequest = chain.request();
Request authenticatedRequest = originalRequest.newBuilder()
.header("Authorization", "Bearer " + token)
.build();
return chain.proceed(authenticatedRequest);
}
}
// Interceptor per il logging delle richieste e risposte
static class LoggingInterceptor implements Interceptor {
@Override
public Response intercept(Chain chain) throws IOException {
Request request = chain.request();
long startTime = System.currentTimeMillis();
System.out.println("Invio richiesta: " + request.url());
Response response = chain.proceed(request);
long duration = System.currentTimeMillis() - startTime;
System.out.println("Risposta ricevuta in " + duration + " ms: " + response.code());
return response;
}
}
public static OkHttpClient buildClient(String authToken) {
return new OkHttpClient.Builder()
.connectTimeout(5, TimeUnit.SECONDS)
.readTimeout(10, TimeUnit.SECONDS)
.addInterceptor(new AuthInterceptor(authToken))
.addInterceptor(new LoggingInterceptor())
.build();
}
}
Utilizzo di Apache HttpClient
Apache HttpClient è una delle librerie HTTP più mature e complete per Java, parte del progetto Apache HttpComponents. È particolarmente adatta in ambienti enterprise per la sua configurabilità avanzata. La dipendenza Maven per la versione 5.x è la seguente:
<dependency>
<groupId>org.apache.httpcomponents.client5</groupId>
<artifactId>httpclient5</artifactId>
<version>5.3.1</version>
</dependency>
Un esempio di richiesta GET e POST con Apache HttpClient 5:
import org.apache.hc.client5.http.classic.methods.HttpGet;
import org.apache.hc.client5.http.classic.methods.HttpPost;
import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
import org.apache.hc.client5.http.impl.classic.HttpClients;
import org.apache.hc.core5.http.ClassicHttpResponse;
import org.apache.hc.core5.http.io.entity.EntityUtils;
import org.apache.hc.core5.http.io.entity.StringEntity;
import java.nio.charset.StandardCharsets;
public class ApacheHttpClientExample {
public static void main(String[] args) throws Exception {
// Il client è AutoCloseable: usarlo in un try-with-resources
try (CloseableHttpClient httpClient = HttpClients.createDefault()) {
// Richiesta GET
HttpGet getRequest = new HttpGet("https://jsonplaceholder.typicode.com/posts/1");
getRequest.addHeader("Accept", "application/json");
String getResponse = httpClient.execute(getRequest, (ClassicHttpResponse response) -> {
// Converte il corpo della risposta in stringa
return EntityUtils.toString(response.getEntity(), StandardCharsets.UTF_8);
});
System.out.println("GET: " + getResponse);
// Richiesta POST con corpo JSON
HttpPost postRequest = new HttpPost("https://jsonplaceholder.typicode.com/posts");
postRequest.addHeader("Content-Type", "application/json");
postRequest.addHeader("Accept", "application/json");
String jsonBody = "{\"title\":\"Post di prova\",\"body\":\"Corpo\",\"userId\":1}";
postRequest.setEntity(new StringEntity(jsonBody, StandardCharsets.UTF_8));
String postResponse = httpClient.execute(postRequest, (ClassicHttpResponse response) -> {
return EntityUtils.toString(response.getEntity(), StandardCharsets.UTF_8);
});
System.out.println("POST: " + postResponse);
}
}
}
Gestione dei cookie
La gestione dei cookie è necessaria quando si interagisce con applicazioni web che mantengono sessioni lato server. Il nuovo HttpClient del JDK supporta la gestione automatica dei cookie tramite un CookieHandler:
import java.net.CookieManager;
import java.net.CookiePolicy;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
public class CookieHandlingExample {
public static void main(String[] args) throws Exception {
// Configura il gestore dei cookie per accettare tutti i cookie
CookieManager cookieManager = new CookieManager(null, CookiePolicy.ACCEPT_ALL);
HttpClient client = HttpClient.newBuilder()
.cookieHandler(cookieManager)
.build();
// Prima richiesta: il server imposta un cookie di sessione
HttpRequest loginRequest = HttpRequest.newBuilder()
.uri(URI.create("https://example.com/login"))
.POST(HttpRequest.BodyPublishers.ofString("user=admin&pass=secret"))
.header("Content-Type", "application/x-www-form-urlencoded")
.build();
client.send(loginRequest, HttpResponse.BodyHandlers.ofString());
// Seconda richiesta: il cookie viene inviato automaticamente
HttpRequest profileRequest = HttpRequest.newBuilder()
.uri(URI.create("https://example.com/profile"))
.GET()
.build();
HttpResponse<String> profileResponse = client.send(
profileRequest,
HttpResponse.BodyHandlers.ofString()
);
System.out.println("Profilo: " + profileResponse.body());
System.out.println("Cookie memorizzati: " + cookieManager.getCookieStore().getCookies());
}
}
Redirect automatici
Per impostazione predefinita il nuovo HttpClient non segue i redirect. Questo comportamento è configurabile tramite la policy di redirect:
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
public class RedirectExample {
public static void main(String[] args) throws Exception {
// Configura il client per seguire automaticamente i redirect HTTP
HttpClient client = HttpClient.newBuilder()
.followRedirects(HttpClient.Redirect.ALWAYS)
.build();
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("http://httpbin.org/redirect/2")) // Restituisce 2 redirect consecutivi
.GET()
.build();
HttpResponse<String> response = client.send(
request,
HttpResponse.BodyHandlers.ofString()
);
// Dopo i redirect, il codice di stato sarà 200
System.out.println("URL finale: " + response.uri());
System.out.println("Codice di stato: " + response.statusCode());
}
}
Download di file
Il HttpClient del JDK include handler specializzati per salvare direttamente la risposta su disco senza caricarla interamente in memoria, operazione fondamentale per file di grandi dimensioni:
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.file.Path;
import java.nio.file.Paths;
public class FileDownloadExample {
public static void downloadFile(String fileUrl, String destinationPath) throws Exception {
HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(fileUrl))
.GET()
.build();
Path targetPath = Paths.get(destinationPath);
// Scrive il corpo della risposta direttamente su file
HttpResponse<Path> response = client.send(
request,
HttpResponse.BodyHandlers.ofFile(targetPath)
);
System.out.println("File salvato in: " + response.body().toAbsolutePath());
System.out.println("Dimensione: " + targetPath.toFile().length() + " byte");
}
public static void main(String[] args) throws Exception {
downloadFile(
"https://www.w3.org/WAI/WCAG21/wcag21.pdf",
"/tmp/wcag21.pdf"
);
}
}
Confronto tra le soluzioni
La scelta dello strumento più adatto dipende dal contesto di progetto. HttpURLConnection è priva di dipendenze esterne e adatta a progetti legacy o utility minimali, ma la sua API è verbosa e poco ergonomica. Il nuovo HttpClient del JDK è la scelta consigliata per progetti su Java 11 o superiore: è moderno, supporta HTTP/2 e le chiamate asincrone senza richiedere librerie aggiuntive. OkHttp eccelle per la ricchezza di funzionalità, il sistema degli interceptor e la naturale integrazione con Android. Apache HttpClient è la scelta preferenziale in ambienti enterprise complessi dove è richiesta una configurazione avanzata di pool, proxy e SSL.
In tutti i casi è buona pratica istanziare il client una sola volta e riutilizzarlo per l'intera vita dell'applicazione, poiché la creazione di un nuovo client ad ogni richiesta è costosa in termini di risorse e annulla i benefici del connection pooling.
Conclusioni
Java offre un ecosistema maturo e variegato per la gestione delle richieste HTTP. Il nuovo HttpClient introdotto in Java 11 copre la maggior parte dei casi d'uso con un'API pulita e moderna, eliminando nella maggior parte dei scenari la necessità di librerie esterne. Per esigenze specifiche come interceptor avanzati, Android, o configurazioni enterprise particolari, OkHttp e Apache HttpClient rimangono scelte solide e ben mantenute.