1. Vue d’ensemble : 4 objets HTTP
Avant de plonger dans les classes spécifiques, il est utile de prendre un moment pour voir toute la structure. Lorsque vous effectuez un appel HTTP à partir de Java, vous ne vous contentez pas de « taper une URL » ; vous assemblez un petit ensemble d’objets : l’adresse (URI), la description de la requête (HttpRequest), l’expéditeur (HttpClient) et le résultat (HttpResponse<T>). Une fois ces rôles bien en tête, le code cesse de ressembler à une incantation magique sur trois écrans et devient une séquence normale d’étapes.
La façon la plus simple de retenir tout cela est de l’imaginer comme une livraison de pizza (encore de la nourriture, mais les programmeurs ne comprennent pas autrement) : URI est l’adresse de livraison, HttpRequest est la commande (ce que nous demandons), HttpClient est le livreur qui se déplace réellement, et HttpResponse<T> est ce qui arrive dans le colis (avec le ticket et le statut, si quelque chose s’est mal passé).
Voici une « carte du terrain » compacte :
| Entité | Description | Utilisation |
|---|---|---|
| URI | objet d’adresse (scheme/host/path/query) | placé dans HttpRequest |
| HttpRequest | description de la requête : méthode, adresse, en-têtes | donné à HttpClient.send() |
| HttpClient | objet qui exécute la requête | effectue l’appel réseau |
| HttpResponse<T> | résultat : statut, en-têtes, corps | on lit et analyse |
Et pour que cela reste en tête non seulement avec des mots, voici un mini-schéma (il est aussi simple que vous le souhaitez pour votre premier appel) :
flowchart LR U["URI"] --> R["HttpRequest"] R --> S["HttpClient.send(...)"] S --> P["HttpResponse<String>"]
Ensuite, nous examinerons chaque brique séparément, très calmement – sans chercher à « faire une belle architecture tout de suite » et sans sauter dans la complexité.
2. URI : l’adresse comme objet
Lorsqu’un débutant écrit son premier appel HTTP, il est presque toujours tenté de faire ceci : « C’est une adresse… donc juste une chaîne. » Et techniquement, c’est vrai – l’adresse peut être stockée dans un String. Mais en pratique, une adresse textuelle devient rapidement fragile : espaces, caractères spéciaux, oubli accidentel du https://, slash superflu, ? ou & erroné, et vous n’êtes plus en train de « faire une requête » mais de « faire une requête dans le vide ».
URI est une classe standard du JDK qui permet de traiter l’adresse comme une structure et non comme un ensemble aléatoire de caractères. Certes, dans le cadre de ce cours, nous allons toujours créer un URI à partir d’une chaîne existante (car vous avez déjà vérifié le contrat dans Postman et vous connaissez l’URL de travail), mais l’important est qu’ensuite, dans le code, ce sera un véritable objet d’adresse.
Un mini-exemple qui montre que URI n’est pas qu’un simple conteneur de texte, mais qu’il sait décomposer l’adresse en parties :
import java.net.URI;
public class UriPartsDemo {
public static void main(String[] args) {
// Création d'un URI à partir d'une chaîne déjà correcte (vérifiée)
URI uri = URI.create("https://catalog.example/search?q=clean%20code&limit=5");
// Extraction des parties de l'adresse — pratique pour le débogage et la compréhension de la structure de l'URL
System.out.println(uri.getScheme()); // https
System.out.println(uri.getHost()); // catalog.example
System.out.println(uri.getPath()); // /search
System.out.println(uri.getQuery()); // q=clean%20code&limit=5
}
}
Notez clean%20code. Ce n’est pas un « ensemble de caractères bizarre », mais un espace encodé. Dans une URL, un espace ne doit pas exister en tant que caractère normal. C’est précisément là qu’on voit pourquoi nous insistions tant sur l’exploration avec Postman : nous n’inventons pas l’adresse à la volée, nous prenons une variante correcte déjà connue. Si vous commencez aujourd’hui à concaténer manuellement une URL avec "q=" + query, vous allez facilement tomber sur des problèmes d’espaces et de caractères comme & ou ?. Donc méthodiquement, il est plus simple pour l’instant : prenez l’URL vérifiée et placez-la dans URI.create(...).
Il y a aussi un petit bonus psychologique : lorsque vous voyez une variable de type URI, votre cerveau la traite automatiquement comme une adresse. Quand vous voyez String url, votre cerveau pense « bon, une chaîne, une chaîne », et ensuite vous êtes surpris qu’elle contienne soudain un saut de ligne ou un espace superflu.
3. HttpClient : l’envoi des requêtes
Après l’adresse, apparaît le prochain héros : HttpClient. C’est un objet du JDK (package java.net.http) qui sait exécuter des requêtes HTTP. Une idée importante : HttpClient n’est pas « une seule requête ». C’est un outil avec lequel vous effectuez plusieurs requêtes. On le crée généralement une seule fois et on le réutilise, plutôt que de le recréer dans chaque méthode (oui, il le permet, mais votre code deviendrait rapidement un film d’horreur d’objets infinis).
Au départ, il suffit de la manière la plus simple de créer un client :
import java.net.http.HttpClient;
public class ClientDemo {
public static void main(String[] args) {
// Création d'un client avec les paramètres par défaut (convient pour une première étape)
HttpClient client = HttpClient.newHttpClient();
// Affichage pédagogique : on vérifie qu'il s'agit d'un objet ordinaire, pas de « magie »
System.out.println("Client created: " + client); // Client created: jdk.internal.net.http.HttpClientImpl@...
}
}
L’affichage du client dans la console ici est purement pédagogique : pour que vous voyiez qu’il s’agit d’un objet ordinaire, pas d’un « bouton magique envoyer ». Dans un projet réel, vous n’êtes bien sûr pas obligé de l’afficher.
Pourquoi disons-nous « créez-le une fois » ? Parce que HttpClient est un objet qui peut contenir des paramètres, des connexions et d’autres détails en interne. Nous n’allons pas entrer dans les subtilités aujourd’hui (l’important est de faire le premier pas), mais l’habitude « un client par application ou par service externe » est très saine.
Si vous souhaitez un peu plus de contrôle (sans complication), vous pouvez créer le client via un builder, même sans définir de paramètres :
import java.net.http.HttpClient;
// Le builder est utile car il rend la « configurabilité » du client explicite dans le code
HttpClient client = HttpClient.newBuilder()
// Ici pourraient se trouver des timeouts, proxy, redirections, etc.
.build();
Le sens ici n’est pas que le builder « est meilleur ». Le sens est de s’habituer : le client est un objet paramétrable, et cela se voit dans le code. Mais je le répète : pour aujourd’hui, HttpClient.newHttpClient() est parfait.
4. HttpRequest : description de la requête
Voici maintenant l’endroit le plus fréquent de confusion : HttpRequest n’est pas « ce qui est déjà parti sur le réseau ». C’est le « plan de la lettre » que nous allons envoyer. Il décrit l’adresse (URI), la méthode (GET) et les en-têtes (par exemple, Accept). Et quand vous appelez build(), vous obtenez un objet de requête immuable : on ne le « retouche » plus, on l’envoie.
La requête est construite via un builder – c’est un patron classique pour les objets qui ont beaucoup de paramètres. Pour l’instant, l’essentiel est de voir trois choses dans le code : URI, la méthode GET et l’en-tête Accept, pour que le serveur comprenne que nous attendons du JSON.
Mini-exemple :
import java.net.URI;
import java.net.http.HttpRequest;
public class RequestDemo {
public static void main(String[] args) {
// Adresse de la requête en tant qu'objet (pas « une chaîne quelconque »)
URI uri = URI.create("https://catalog.example/search?q=clean%20code");
// Construction d'un HttpRequest immuable : méthode, en-têtes, URI
HttpRequest request = HttpRequest.newBuilder(uri)
// Indiquer au serveur que nous attendons du JSON (partie du contrat)
.header("Accept", "application/json")
// Spécifier explicitement la méthode pour ne pas compter sur des « valeurs par défaut »
.GET()
.build();
// Vérification pour soi : la méthode est bien GET
System.out.println("Request method: " + request.method()); // Request method: GET
}
}
Plusieurs petites observations « d’adulte » sont importantes ici.
Premièrement, la méthode est spécifiée explicitement (GET), et c’est bien. Même si HttpRequest a des valeurs par défaut, il ne faut pas s’habituer à « il devinera tout seul ». Nous construisons des habitudes backend où le contrat est visible.
Deuxièmement, l’en-tête Accept fait partie du contrat. Si vous l’aviez défini dans Postman, il est logique de faire de même dans le code. C’est comme si nous disions au catalogue externe : « Je veux la réponse en JSON. » Oui, parfois le serveur renvoie du JSON même sans cela, mais vous apprenez maintenant à être un client discipliné, pas à « essayer sans en-têtes, on verra bien ».
Troisièmement, HttpRequest est un objet qu’il est pratique de passer à la méthode d’envoi. Il ressemble à une « instruction pour le livreur » : où aller et quoi livrer.
5. HttpResponse<T> et BodyHandler<T>
Une fois la requête envoyée, la partie la plus intéressante est la réponse. Et c’est là que beaucoup de débutants s’attendent à ce que « réponse = chaîne ». Mais une réponse HTTP comporte au moins trois parties : le code de statut, les en-têtes et le corps. Et le JDK vous fournit honnêtement un objet qui contient tout cela : HttpResponse<T>.
La lettre T dans HttpResponse<T> n’est pas décorative. Elle indique : « sous quelle forme le corps de la réponse sera lu ». Autrement dit, HttpResponse<String> signifie : nous voulons que le body soit lu comme une chaîne. HttpResponse<byte[]> signifierait : lu comme des octets. Pour aujourd’hui, nous avons besoin de la version la plus simple et la plus visuelle – une chaîne, car nous voulons voir le JSON brut sans nous compliquer la vie.
Pour que le JDK comprenne comment lire le corps, nous passons un objet-instruction spécial – BodyHandler<T>. Cela semble plus effrayant qu’en réalité. Dans le sens courant, BodyHandler est la « façon de lire le body ». La variante de départ la plus courante :
HttpResponse.BodyHandlers.ofString()
Construisons maintenant un fragment court mais complet : il y a client, il y a request, nous exécutons send() et obtenons HttpResponse<String>. Je laisse intentionnellement throws Exception pour ne pas faire un cours séparé sur les exceptions vérifiées et la gestion des erreurs – notre objectif est de voir la mécanique.
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
public class ResponseDemo {
public static void main(String[] args) throws Exception {
// Le client est généralement créé une fois et réutilisé
HttpClient client = HttpClient.newHttpClient();
// Requête : vers où, avec quelle méthode, et ce que nous attendons
HttpRequest request = HttpRequest.newBuilder()
.uri(java.net.URI.create("https://catalog.example/search?q=clean%20code"))
.header("Accept", "application/json")
.GET()
.build();
// Envoi : le second argument indique comment lire le corps de la réponse (ici comme chaîne)
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
// Le statut est aussi important que le corps
System.out.println("HTTP " + response.statusCode()); // par exemple : HTTP 200
// Le corps est simplement une chaîne (peut contenir du JSON, mais « ne se transforme pas » tout seul en objet)
System.out.println("Body length = " + response.body().length()); // par exemple : Body length = 1248
}
}
Ici, vous voyez la « liaison » clé qu’il est important de retenir visuellement (pas par cœur) : le type HttpResponse<String> est directement lié à BodyHandlers.ofString(). Ce n’est pas une coïncidence, c’est le contrat entre vous et le JDK : vous dites à l’avance comment vous voulez lire le corps.
Un autre point qu’il est agréable de comprendre à l’avance : send() renvoie un HttpResponse même si le statut est un échec (par exemple, 404 ou 500). C’est toujours une réponse du serveur, juste pas celle que nous voulions. Autrement dit, « statut d’échec » n’est pas nécessairement une « exception ». Les exceptions concernent plutôt une autre classe de problèmes : quand vous n’avez pas reçu de réponse du tout (par exemple, le réseau est mort). Aujourd’hui, nous ne développons pas ce sujet, mais il est utile de ne pas confondre ces deux réalités : « le serveur a répondu avec une erreur » et « le serveur n’a pas répondu ».
Si vous voulez voir les en-têtes (les en-têtes font partie du contrat, on en a déjà parlé), vous pouvez par exemple récupérer Content-Type :
import java.net.http.HttpResponse;
// Les en-têtes peuvent manquer, donc on utilise Optional
String contentType = response.headers()
.firstValue("Content-Type")
// Si l'en-tête est absent, on le signale explicitement dans le résultat
.orElse("<no Content-Type>");
System.out.println("Content-Type = " + contentType); // Content-Type = application/json; charset=utf-8
Pourquoi Optional et orElse ici ? Parce que l’en-tête peut être absent. HTTP est conçu de manière à devoir vivre dans un monde où « il peut y en avoir, il peut ne pas y en avoir ». Ce n’est pas un bug – c’est la réalité du contrat réseau.
6. Mini-chaîne : URI → HttpRequest → send()
Vous avez déjà vu les morceaux séparément. Rassemblons maintenant le « squelette » sous la forme d’une petite méthode qui fait une chose compréhensible : elle prend un URI, envoie un GET et retourne un HttpResponse<String>. Nous ne construisons pas de couches, pas de « framework universel », pas de fabriques ni d’abstractions. Nous faisons simplement en sorte que la séquence soit transparente et lisible.
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
public class SimpleGetSender {
// Le client – comme un « livreur » : vit généralement longtemps et est réutilisé
private final HttpClient client = HttpClient.newHttpClient();
public HttpResponse<String> getJson(URI uri) throws Exception {
// La requête est créée à chaque appel : c'est une « commande » spécifique
HttpRequest request = HttpRequest.newBuilder(uri)
// Contrat : nous voulons obtenir du JSON
.header("Accept", "application/json")
.GET()
.build();
// Envoi et lecture du corps comme chaîne
return client.send(request, HttpResponse.BodyHandlers.ofString());
}
}
Remarquez : cette classe n’est ni « belle » ni « finale ». Elle est pédagogique et honnête. On y voit tout l’essentiel : quelle méthode, quel en-tête, comment nous lisons le body. C’est exactement ce qu’il faut pour les premiers pas – pour comprendre la mécanique, sans essayer de mémoriser « le bon ensemble de lignes ».
7. Nuances importantes
Après le premier morceau de code fonctionnel, il est très tentant de faire deux mouvements dangereux : soit « tout complexifier », soit « tout simplifier au point de perdre le sens ». Pour éviter l’un et l’autre, il est utile de retenir plusieurs nuances dès maintenant – non pas comme des règles séparées, mais comme des observations calmes.
Premièrement, HttpClient, HttpRequest et URI sont des objets ordinaires. Oui, ils sont liés au réseau, mais le code autour d’eux n’est pas obligé d’être de la « magie réseau ». Si vous pouvez afficher request.method() ou uri.getHost(), vous êtes dans un monde normal d’entités typées, pas dans l’ésotérisme.
Deuxièmement, HttpRequest est construit par un builder précisément parce que la requête comporte beaucoup de parties. Aujourd’hui, nous ne définissons que URI, Accept et GET. Mais même sous cette forme, HttpRequest vous aide à garder le contrat à un seul endroit, sans disperser les « morceaux de requête » dans le code.
Troisièmement, HttpResponse<String> n’est pas une « chaîne ». C’est un objet qui a statusCode(), headers() et body(). Si vous prenez l’habitude de ne lire que body(), vous passerez à côté de la moitié du sens de HTTP. Et ensuite, quand quelque chose se cassera, vous aurez un diagnostic de niveau « ça ne marche pas », ce qui, pour un développeur backend, n’est pas un diagnostic mais une raison de boire tristement du thé.
8. Erreurs typiques avec le client HTTP du JDK
Erreur n°1 : confondre HttpClient et HttpRequest, comme s’ils étaient la même chose.
C’est une confusion courante : on pense que « le client, c’est la requête ». En réalité, le client est un outil, et la requête est une description spécifique de l’action. Si vous gardez en tête l’image du « livreur » et de la « commande », cela devient plus simple : un seul livreur, plusieurs commandes. Dans le code, cela se résout généralement en faisant vivre HttpClient comme un champ de l’objet ou en le créant une seule fois, tandis que HttpRequest est créée à chaque appel.
Erreur n°2 : écrire l’adresse comme une chaîne avec des espaces et « espérer que ça passera ».
Lorsqu’un espace, une lettre russe ou le symbole & se retrouve dans la valeur d’un paramètre, la concaténation devient une loterie. Parfois « ça semble marcher », puis ça casse soudainement sur une autre requête. Même si vous créez un URI via URI.create(...), assurez-vous que la chaîne est correcte et déjà vérifiée (par exemple, celle que vous avez vue dans Postman). Et n’y ajoutez pas « un petit quelque chose » tant que vous ne savez pas encoder correctement les paramètres.
Erreur n°3 : s’attendre à ce que HttpResponse soit directement un « objet JSON ».
Si vous lisez la réponse via BodyHandlers.ofString(), alors body() n’est qu’une chaîne. Elle peut contenir du JSON, mais Java n’en « comprend » pas encore la structure. C’est normal, et même utile pour un premier pas : vous voyez la réponse brute telle qu’elle est arrivée, et vous pouvez la comparer à vos attentes du contrat.
Erreur n°4 : oublier BodyHandlers.ofString() et s’étonner de « ne pas pouvoir obtenir le body ».
HttpClient.send() exige deux arguments : la HttpRequest elle-même et la manière de lire le corps (BodyHandler). Si vous avez oublié BodyHandlers.ofString(), vous n’avez pas dit au JDK quoi faire du body. Ce n’est pas un caprice de l’API – c’est une exigence honnête : le corps peut être lu de différentes façons, et vous devez choisir.
Erreur n°5 : analyser la réponse uniquement via body(), en ignorant statusCode().
Parfois, le serveur envoie un beau JSON même en cas d’erreur. Par exemple, un 404 avec un corps JSON { "error": "not found" }. Si vous ne regardez que le body, vous pouvez penser que tout va bien (« ouah, du JSON est arrivé ! »), puis construire un code qui traitera joyeusement une erreur comme une réponse réussie. D’où l’habitude simple : d’abord le statut, ensuite les en-têtes (au moins parfois), puis le corps.
GO TO FULL VERSION