Ici se réalisent les projets

Vous travaillez sur la récupération de données un jour comme un autre et là, BAM, ça ne vous renvoie pas du 200 (OK) ou du 202 (SUCCESS) ou du 418 (I’M A TEAPOT) mais du partial content (206, comme la Renault).

Comment récupérer vos données à partir de là ? 

C’est l’objet de cet article \o/

Détecter un partial-content

Quand vous recevez une réponse 206, outre ce 206, il devrait y avoir caché dans le header de la réponse une clé Content-Range avec la pagination du résultat (par exemple : Content-Range : offres 0-149/172) et une clé Accept-Range avec la valeur max de résultats que l’API vous autorise à récupérer (par exemple : Accept-Range : 150).

Sinon, à vous de vous renseigner auprès de l’API requêtée pour connaitre ses limites de récupération ¯\(°_o)/¯

Récupérer un partial-content

Il vous faudra ensuite écrire une requête légèrement différente, avec à la fin par exemple le mot clé range (ce mot clé peut varier selon les API, lisez la doc, vive la doc).

https://api.francetravail.io/partenaire/offresdemploi/v2/offres/search?motsCles=champignon&range=0-149

Ce range permet de récupérer les résultats compris entre la page 0 et la page 149.

L’astuce est ensuite de créer un tout petit peu de code autour pour boucler jusqu’à la dernière page. Notez que normalement, vous connaissez la dernière page parce qu’elle vous est renvoyé dans le header de la réponse (souvenez vous, le fameux Content-Range, dans ce cas précis, il renvoyait offres 0-149/172 donc la dernière page est la page 172).

Une solution possible est d’incrémenter un compteur, qui sera utilisé dans l’url. Tant que ce compteur est inférieur à la dernière page (ici 172) une nouvelle requête sera renvoyée (avec un while par exemple) en incrémentant selon le Accept-Range jusqu’à avoir tout récupéré (attention à ne pas dépasser ce range ou l’API ne sera pas contente et refusera de vous servir 🥺). 

Au sein de cette boucle, chaque fois qu’on a une réponse qui avance dans la boucle, on ajoute les résultats à une liste. Ensuite on peux return cette liste ou en faire ce qu’on veut, et on a tous les résultats cette fois \o/

   url = https://ma-requete
   response = request(url)

    if (response.code == 206) {
       liste_de_resultat = new List()
       offset = 0;

       // si le résultat max est écrit sous la forme 0-149/172
       total_items = response.header(Content-Range.Split(“/”)[1]) 

       limit = response.header(Accept-Range)

       while (offset < total_items) {
           url = https://ma-requete&range=offset - offset + limit - 1
           response = request(url)

           liste_de_resultat.add(response.body)

           offset += limit - 1
        }
    }

   

Exemple d’implémentation en C# (Blazor Server)

    public async Task<ApiResponse?> GetOffresFromKeyword(string? keyword) {
        ApiResponse descriptions = new ApiResponse();

        int offset = 0;
        int limit = 100; // Taille max du tronçon : 149

        string requestUrl = 
            $"https://api.francetravail.io/partenaire/offresdemploi" +
            $"/v2/offres/search?motsCles={keyword}";

        while (offset < totalItems) {
            // Pagination si nécessaire
            if (offset > 0)
                requestUrl += $"&range={offset}-{offset + limit - 1}";

            request = new HttpRequestMessage {
                Method = HttpMethod.Get,
                RequestUri = new Uri(requestUrl),
                Headers = {
                    { "Authorization", "Bearer " + token },
                    { "Accept", "application/json" }
                }
            };

            // Effectuer la requête
            response = await _httpClient.SendAsync(request);
            
            // Gérer les erreurs serveur
            if (response.StatusCode == HttpStatusCode.InternalServerError) {
                if (retryCounter >= 5) break;
                retryCounter++;
                token = await GetAccessToken();
                continue;
            }

            // Lire le contenu de la réponse
            var content = await response.Content.ReadAsStringAsync();

            if (response.StatusCode == HttpStatusCode.PartialContent || 
                response.StatusCode == HttpStatusCode.OK) {
                try {
                    // sometimes serialization fail, we still must retrieve
                    // as many result as possible, so return
                    partialData = JsonConvert.DeserializeObject<ApiResponse>(content);
                    if (partialData?.Resultats != null) {
                        descriptions.Resultats ??= 
                            new List<FranceTravailOffreDescription>();
                        descriptions.Resultats.AddRange(partialData.Resultats);
                    }
                }
                catch (Exception) {
                    return descriptions;
                }

                // Extraire le total des éléments (si non encore défini)
                if (response.Content.Headers.Contains("Content-Range")) {
                    contentRange = response.Content.Headers.
                        GetValues("Content-Range").
                        FirstOrDefault();

                    if (!string.IsNullOrEmpty(contentRange)) {
                        totalItems = int.Parse(contentRange.Split('/').Last());
                    }
                }
            }
            else {
                return descriptions;
            }

            // Mettre à jour l'offset
            offset += limit;
        }

        return descriptions;
    }

Gestion et prévention des erreurs

Ça en fait des occasions ou ça pourrait ne pas fonctionner. Pensons donc à gérer chaque erreur possible pour ne pas faire planter notre application.

  • l’API ne répond plus (renvoyer les partial content qu’on a réussi à chopper tant bien que mal peut être une solution).
  • Le résultat devient vraiment trop gros pour être stocké ensuite (attention à bien prévisionner en checkant le nombre total de pages en amont et en prenant une décision sage sur ce qu’on est prêt à accepter).


Leave a Reply