Tag Archives: HttpClient

Meilleure gestion du timeout avec HttpClient

Le problème

Si vous avez l’habitude d’utiliser HttpClient pour appeler des APIs REST ou transférer des fichiers, vous avez peut-être déjà pesté contre la façon dont cette classe gère le timeout. Il y a en effet deux problèmes majeurs dans la gestion du timeout par HttpClient :

  • Le timeout est défini de façon globale, et s’applique à toutes les requêtes, alors qu’il serait plus pratique de pouvoir le définir individuellement pour chaque requête.
  • L’exception levée quand le temps imparti est écoulé ne permet pas de déterminer la cause de l’erreur. En effet, en cas de timeout, on s’attendrait à recevoir une TimeoutException, non ? Eh bien, surprise, c’est une TaskCanceledException qui est levée! Du coup, impossible de savoir si la requête a réellement été annulée, ou si le timeout est écoulé.

Heureusement, tout n’est pas perdu, et la flexibilité de HttpClient va permettre de compenser cette petite erreur de conception…

On va donc implémenter un mécanisme permettant de pallier les deux problèmes mentionnés plus haut. On souhaite donc :

  • pouvoir spécifier un timeout différent pour chaque requête
  • recevoir une TimeoutException plutôt que TaskCanceledException en cas de timeout

Spécifier le timeout pour une requête

Voyons d’abord comment associer une valeur de timeout à une requête. La classe HttpRequestMessage a une propriété Properties, qui est un dictionnaire dans lequel on peut mettre ce qu’on veut. On va donc l’utiliser pour stocker le timeout pour une requête, et pour faciliter les choses, on va créer des méthodes d’extension pour accéder à la valeur de façon fortement typée :

public static class HttpRequestExtensions
{
    private static string TimeoutPropertyKey = "RequestTimeout";

    public static void SetTimeout(
        this HttpRequestMessage request,
        TimeSpan? timeout)
    {
        if (request == null)
            throw new ArgumentNullException(nameof(request));

        request.Properties[TimeoutPropertyKey] = timeout;
    }

    public static TimeSpan? GetTimeout(this HttpRequestMessage request)
    {
        if (request == null)
            throw new ArgumentNullException(nameof(request));

        if (request.Properties.TryGetValue(
                TimeoutPropertyKey,
                out var value)
            && value is TimeSpan timeout)
            return timeout;
        return null;
    }
}

Rien de très compliqué ici, le timeout est une valeur optionnelle de type TimeSpan. Évidemment il n’y a pour l’instant aucun code pour tenir compte du timeout associé à une requête…

Handler HTTP

L’architecture de HttpClient est basée sur un système de pipeline : chaque requête est envoyée à travers une chaîne de handlers (de type HttpMessageHandler), et la réponse repasse en sens inverse à travers cette chaîne. Cet article rentre un peu plus dans le détail si vous voulez en savoir plus. Nous allons donc insérer dans le pipeline notre propre handler, qui sera chargé de la gestion du timeout.

Notre handler va hériter de DelegatingHandler, un type de handler conçu pour être chaîné à un autre handler. Pour implémenter un handler, il faut redéfinir la méthode SendAsync. Une implémentation minimale ressemblerait à ceci :

class TimeoutHandler : DelegatingHandler
{
    protected async override Task<HttpResponseMessage> SendAsync(
        HttpRequestMessage request,
        CancellationToken cancellationToken)
    {
        return await base.SendAsync(request, finalCancellationToken);
    }
}

L’appel à base.SendAsync va simplement passer la requête au handler suivant. Du coup, pour l’instant notre implémentation ne sert à rien, mais on va l’enrichir petit à petit.

Prendre en compte le timeout pour une requête

Ajoutons d’abord à notre classe une propriété DefaultTimeout, qui sera utilisée pour les requêtes dont le timeout n’est pas explicitement défini :

public TimeSpan DefaultTimeout { get; set; } = TimeSpan.FromSeconds(100);

La valeur par défaut de 100 secondes est la même que celle de HttpClient.Timeout.

Pour implémenter le timeout, on va récupérer la valeur associée à la requête (ou à défaut DefaultTimeout), créer un CancellationToken qui sera annulé après la durée du timeout, et passer ce CancellationToken au handler suivant : la requête sera donc annulée après l’expiration de ce délai (ce qui correspond au comportement par défaut de HttpClient).

Pour créer un CancellationToken dont on peut contrôler l’annulation, on utilise un objet CancellationTokenSource, qu’on va créer comme ceci en fonction du timeout de la requête :

private CancellationTokenSource GetCancellationTokenSource(
    HttpRequestMessage request,
    CancellationToken cancellationToken)
{
    var timeout = request.GetTimeout() ?? DefaultTimeout;
    if (timeout == Timeout.InfiniteTimeSpan)
    {
        // No need to create a CTS if there's no timeout
        return null;
    }
    else
    {
        var cts = CancellationTokenSource
            .CreateLinkedTokenSource(cancellationToken);
        cts.CancelAfter(timeout);
        return cts;
    }
}

Deux choses à noter ici :

  • si le timeout de la requête est infini, on ne crée pas de CancellationTokenSource; il ne servirait à rien puisqu’il ne serait jamais annulé, on économise donc une allocation inutile.
  • Dans le cas contraire, on crée un CancellationTokenSource qui sera annulé après expiration du timeout (CancelAfter). Notez que ce CTS est lié au CancellationToken reçu en paramètre de SendAsync: il sera donc annulé soit par après expiration du timeout, soit quand ce CancellationToken sera lui-même annulé. Je vous renvoie à cet article pour plus d’infos à ce sujet.

Enfin, modifions la méthode SendAsync pour prendre en compte le CancellationTokenSource qu’on a créé :

protected async override Task<HttpResponseMessage> SendAsync(
    HttpRequestMessage request,
    CancellationToken cancellationToken)
{
    using (var cts = GetCancellationTokenSource(request, cancellationToken))
    {
        return await base.SendAsync(
            request,
            cts?.Token ?? cancellationToken);
    }
}

On récupère le CTS, et on passe son token à base.SendAsync. Notez qu’on utilise cts?.Token puisque GetCancellationTokenSource peut renvoyer null; si c’est le cas, on utilise le CancellationToken reçu en paramètre.

À ce stade, on a un handler qui permet de spécifier un timeout différent pour chaque requête. Mais il reste le problème de l’exception renvoyée en cas de timeout, qui est encore une TaskCanceledException… Mais on va régler ça très facilement!

Lever la bonne exception

En effet, il suffit d’intercepter l’exception TaskCanceledException (ou plutôt sa classe de base, OperationCanceledException), et de vérifier si le CancellationToken reçu en paramètre est annulé: si oui, l’annulation vient de l’appelant, et on laisse l’exception se propager normalement; si non, c’est qu’elle est causée par le timeout, et dans ce cas on lance une TimeoutException. Voilà donc notre méthode SendAsync finale:

protected async override Task<HttpResponseMessage> SendAsync(
    HttpRequestMessage request,
    CancellationToken cancellationToken)
{
    using (var cts = GetCancellationTokenSource(request, cancellationToken))
    {
        try
        {
            return await base.SendAsync(
                request,
                cts?.Token ?? cancellationToken);
        }
        catch(OperationCanceledException)
            when (!cancellationToken.IsCancellationRequested)
        {
            throw new TimeoutException();
        }
    }
}

On utilise ici un filtre d’exception : cela évite d’intercepter OperationCanceledException si on doit la laisser se propager; on évite ainsi de dérouler la pile inutilement.

Notre handler est terminé, voyons maintenant comment l’utiliser.

Utilisation du handler

Quand on crée un HttpClient, il est possible de passer en paramètre du constructeur le premier handler du pipeline. Si on ne spécifie rien, par défaut c’est un HttpClientHandler qui est créé; ce handler envoie directement les requêtes vers le réseau. Pour utiliser notre nouveau TimeoutHandler, on va le créer, lui attacher un HttpClientHandler comme handler suivant, et le passer au HttpClient:

var handler = new TimeoutHandler
{
    InnerHandler = new HttpClientHandler()
};

using (var client = new HttpClient(handler))
{
    client.Timeout = Timeout.InfiniteTimeSpan;
    ...
}

Notez qu’il faut désactiver le timeout du HttpClient en lui donnant une valeur infinie, sinon le comportement par défaut viendra interférer avec notre handler.

Essayons maintenant d’envoyer une requête avec un timeout de 5 secondes vers un serveur qui met trop longtemps à répondre:

var request = new HttpRequestMessage(HttpMethod.Get, "http://foo/");
request.SetTimeout(TimeSpan.FromSeconds(5));
var response = await client.SendAsync(request);

Si le serveur n’a pas répondu au bout de 5 secondes, on obtiendra bien une TimeoutException, et non une TaskCanceledException.

Vérifions maintenant que le cas de l’annulation marche toujours correctement. Pour cela, on va passer un CancellationToken qui sera annulé au bout de 2 secondes (avant expiration du timeout, donc) :

var request = new HttpRequestMessage(HttpMethod.Get, "http://foo/");
request.SetTimeout(TimeSpan.FromSeconds(5));
var cts = new CancellationTokenSource(TimeSpan.FromSeconds(2));
var response = await client.SendAsync(request, cts.Token);

Et on obtient bien une TaskCanceledException!

En implémentant notre propre handler HTTP, on a donc pu régler notre problème de départ et avoir une gestion intelligente du timeout.

Le code complet de cet article est disponible ici.

Tout faire ou presque avec le pipeline de HttpClient

Il y a quelques années, Microsoft a introduit la classe HttpClient comme alternative moderne à HttpWebRequest pour faire des requêtes web depuis des applications .NET. Non seulement cette nouvelle API est beaucoup plus facile à utiliser, plus propre, et asynchrone, mais elle est aussi facilement extensible.

Vous avez peut-être remarqué que HttpClient a un constructeur qui accepte un HttpMessageHandler. De quoi s’agit-il ? Un HttpMessageHandler est un objet qui accepte une requête (HttpRequestMessage) et renvoie une réponse (HttpResponseMessage) ; la façon dont il le fait dépend complètement de l’implémentation. Par défaut, HttpClient utilise HttpClientHandler, un handler qui envoie une requête vers un serveur sur le réseau et renvoie la réponse du serveur. L’autre implémentation fournie de HttpMessageHandler est une classe abstraite nommée DelegatingHandler, et c’est de celle là que je voudrais parler.

Le pipeline

DelegatingHandler est un handler qui est conçu pour être chaîné à un autre handler, ce qui donne un pipeline à travers lequel les requêtes et réponses vont passer, comme illustré par ce schéma :

Schéma du pipeline de HttpClient

(Image tirée du site officiel ASP.NET)

Chaque handler a la possibilité d’examiner et/ou de modifier la requête avant de la passer au handler suivant, et d’examiner et/ou de modifier la réponse reçue du handler suivant. Habituellement, le dernier handler dans le pipeline est le HttpClientHandler, qui communique directement avec le réseau.

La chaîne de handlers peut être configurée comme ceci :

var pipeline = new MyHandler1()
{
    InnerHandler = new MyHandler2()
    {
        InnerHandler = new HttpClientHandler()
    }
};
var client = new HttpClient(pipeline);

Mais si vous préférez les interfaces “fluent”, il est facile de créer une méthode d’extension qui permet de le faire comme ceci :

var pipeline = new HttpClientHandler()
    .DecorateWith(new MyHandler2())
    .DecorateWith(new MyHandler1());
var client = new HttpClient(pipeline);

Tout ça semble peut-être un peu abstrait pour l’instant, mais cette architecture à base de pipeline rend possible plein de scénarios intéressants. En effet, ces handlers de messages HTTP peuvent être utilisés pour ajouter des comportements personnalisés au traitement des requêtes et réponses. Je vais en donner quelques exemples.

Remarque : Je présente cette fonctionnalité d’un point de vue client (vu que je développe essentiellement des applis clientes), mais le même système de handlers est également utilisé côté serveur dans ASP.NET Web API.

Tests unitaires

Le premier cas d’utilisation qui vient à l’esprit, et le premier que j’ai mis en oeuvre, c’est les tests unitaires. Si vous testez une classe qui fait des paiements en ligne via HTTP, vous ne voulez pas que vos tests envoient réellement des requêtes au vrai serveur… Vous voulez juste vous assurer que les requêtes envoyées sont correctes, et que le code réagit correctement à des réponses spécifiques. Une solution simple à ce problème est de créer un handler “stub”, et de l’injecter dans votre classe à la place de HttpClientHandler. Voici une implémentation possible :

class StubHandler : HttpMessageHandler
{
    // Responses to return
    private readonly Queue<HttpResponseMessage> _responses =
        new Queue<System.Net.Http.HttpResponseMessage>();

    // Requests that were sent via the handler
    private readonly List<HttpRequestMessage> _requests =
        new List<System.Net.Http.HttpRequestMessage>();

    protected override Task<HttpResponseMessage> SendAsync(
        HttpRequestMessage request,
        CancellationToken cancellationToken)
    {
        if (_responses.Count == 0)
            throw new InvalidOperationException("No response configured");

        _requests.Add(request);
        var response = _responses.Dequeue();
        return Task.FromResult(response);
    }

    public void QueueResponse(HttpResponseMessage response) =>
        _responses.Enqueue(response);

    public IEnumerable<HttpRequestMessage> GetRequests() =>
        _requests;
}

Cette classe permet d’enregister les requêtes qui sont envoyées via le handler et de spécifier les réponses qui doivent être renvoyées. Par exemple, on pourrait écrire un test comme celui-ci :

// Arrange
var handler = new StubHandler();
handler.EnqueueResponse(new HttpResponseMessage(HttpStatusCode.Unauthorized));
var processor = new PaymentProcessor(handler);

// Act
var paymentResult = await processor.ProcessPayment(new Payment());

// Assert
Assert.AreEqual(PaymentStatus.Failed, paymentResult.Status);

Bien sûr, plutôt que de créer un stub manuellement, il est possible d’utiliser un framework de mock pour générer un faux handler. Le fait que la méthode SendAsync soit protégée rend cette approche un peu moins facile qu’elle devrait l’être, mais on peut facilement contourner le problème en créant une classe dérivée qui expose une méthode publique virtuelle, et en faisant un mock de cette classe :

public abstract class MockableMessageHandler : HttpMessageHandler
{
    protected override sealed Task<HttpResponseMessage> SendAsync(
        HttpRequestMessage request,
        CancellationToken cancellationToken)
    {
        return DoSendAsync(request);
    }

    public abstract Task<HttpResponseMessage> DoSendAsync(HttpRequestMessage request);
}

Exemple d’utilisation avec FakeItEasy :

// Arrange
var handler = A.Fake<MockableMessageHandler>();
A.CallTo(() => handler.DoSendAsync(A<HttpRequestMessage>._))
    .Returns(new HttpResponseMessage(HttpStatusCode.Unauthorized));
var processor = new PaymentProcessor(handler);
...

Logging

Écrire dans le log les requêtes envoyées et les réponses reçues peut aider à diagnostiquer certains problèmes. C’est très facile à mettre en oeuvre avec un un DelegatingHandler personnalisé :

public class LoggingHandler : DelegatingHandler
{
    private readonly ILogger _logger;

    public LoggingHandler(ILogger logger)
    {
        _logger = logger;
    }

    protected override async Task<HttpResponseMessage> SendAsync(
        HttpRequestMessage request,
        CancellationToken cancellationToken)
    {
        _logger.Trace($"Request: {request}");
        try
        {
            // base.SendAsync calls the inner handler
            var response = await base.SendAsync(request, cancellationToken);
            _logger.Trace($"Response: {response}");
            return response;
        }
        catch (Exception ex)
        {
            _logger.Error($"Failed to get response: {ex}");
            throw;
        }
    }
}

Réessayer les requêtes échouées

Un autre cas d’utilisation intéressant des handlers de messages HTTP est de réessayer automatiquement les requêtes qui ont échouées. Par exemple, le serveur auquel on s’adresse peut être temporairement indisponible (503), il peut limiter nos requêtes (429), ou on peut tout simplement avoir perdu l’accès à internet. Dans toutes ces situations, réessayer la même requête plus tard a de bonnes chances de fonctionner (le serveur peut avoir redémarré, on peut avoir retrouvé du wifi…). Gérer la retentative au niveau du code applicatif est laborieux, car ça peut se produire pratiquement n’importe où. Avoir cette logique au plus bas niveau possible et implémentée d’une façon complètement transparente pour l’appelant rend les choses beaucoup plus simples.

Voici une implémentation possible d’un handler qui réessaie les requêtes échouées :

public class RetryHandler : DelegatingHandler
{
    protected override async Task<HttpResponseMessage> SendAsync(
        HttpRequestMessage request,
        CancellationToken cancellationToken)
    {
        while (true)
        {
            try
            {
                // base.SendAsync calls the inner handler
                var response = await base.SendAsync(request, cancellationToken);

                if (response.StatusCode == HttpStatusCode.ServiceUnavailable)
                {
                    // 503 Service Unavailable
                    // Wait a bit and try again later
                    await Task.Delay(5000, cancellationToken);
                    continue;
                }

                if (response.StatusCode == (HttpStatusCode)429)
                {
                    // 429 Too many requests
                    // Wait a bit and try again later
                    await Task.Delay(1000, cancellationToken);
                    continue;
                }

                // Not something we can retry, return the response as is
                return response;
            }
            catch (Exception ex) when(IsNetworkError(ex))
            {
                // Network error
                // Wait a bit and try again later
                await Task.Delay(2000, cancellationToken);
                continue;
            }
        }
    }

    private static bool IsNetworkError(Exception ex)
    {
        // Check if it's a network error
        if (ex is SocketException)
            return true;
        if (ex.InnerException != null)
            return IsNetworkError(ex.InnerException);
        return false;
    }
}

Remarquez que cette implémentation est un peu simpliste ; pour l’utiliser dans du code de production, il faudra sans doute ajouter un “exponential backoff” (attendre de plus en plus longtemps entre chaque tentative), prendre en compte l’en-tête Retry-After pour déterminer combien de temps il faut attendre, ou encore déterminer de façon un peu plus subtile si une exception correspond à une erreur réseau. De plus, en l’état actuel, ce handler réessaiera indéfiniment jusqu’à ce que la requête réussisse ; assurez-vous donc de passer un jeton d’annulation (CancellationToken) pour pouvoir l’arrêter si besoin.

Autres cas d’utilisations

Je ne peux pas donner d’exemples pour chaque scénario possible, mais voilà quelques autres cas d’utilisation possible des handlers de messages HTTP :

  • Gestion personnalisée des cookies (quelque chose que j’ai réellement fait, pour contourner un bug dans CookieContainer)
  • Authentification personnalisée (également quelque chose que j’ai fait, pour implémenter l’authentification OAuth2)
  • Utiliser l’en-tête X-HTTP-Method-Override pour passer les proxy qui refusent certaines méthode HTTP (voir l’article de Scott Hanselman pour plus de détails)
  • Chiffrement ou encodage personnalisé
  • Mise en cache

Comme vous le voyez, il y a tout un monde de possibilités. Si vous avez d’autres idées, indiquez les dans les commentaires !

Envoyer des données avec HttpClient selon un modèle “push”

Si vous avez déjà utilisé la classe HttpWebRequest pour envoyer des données, vous savez qu’elle utilise un modèle “push”. Ce que j’entends par là, c’est que vous appelez la méthode GetRequestStream, qui ouvre la connexion si nécessaire, envoie les en-têtes, et renvoie un flux sur lequel vous pouvez écrire directement.

.NET 4.5 a introduit la classe HttpClient comme nouveau moyen de communiquer en HTTP. Elle repose en fait sur HttpWebRequest en interne, mais offre une API plus pratique et complètement asynchrone. HttpClient utilise une approche différente en ce qui concerne l’upload de données : au lieu d’écrire directement sur le flux de la requête, il faut affecter à la propriété Content du HttpRequestMessage une instance d’une classe dérivée de HttpContent. On peut également passer le contenu directement aux méthodes PostAsync ou PutAsync.

Le framework .NET fournit quelques implémentations standard de HttpContent, voici quelques unes des plus communément utilisées :

  • ByteArrayContent: représente du contenu binaire brut en mémoire
  • StringContent: représente du texte avec un encodage spécifique (c’est une spécialisation de ByteArrayContent)
  • StreamContent: représente des données binaires brutes sous forme d’un flux

Par exemple, voilà comment on peut uploader le contenu d’un fichier :

async Task UploadFileAsync(Uri uri, string filename)
{
    using (var stream = File.OpenRead(filename))
    {
        var client = new HttpClient();
        var response = await client.PostAsync(uri, new StreamContent(stream));
        response.EnsureSuccessStatusCode();
    }
}

Comme vous l’aurez peut-être remarqué, ce code n’écrit jamais explicitement sur le flux de la requête : le contenu est automatiquement lu depuis le flux source et copié vers le flux de la requête.

Ce modèle “pull” convient à la plupart des usages, mais il a un inconvénient : il nécessite que les données à envoyer existent déjà sous une forme qui peut être directement envoyée au serveur. Ce n’est pas toujours souhaitable, parce que parfois on veut générer “à la volée” le contenu de la requête. Par exemple, si on veut envoyer un objet sérialisé en JSON, avec l’approche “pull”, il faut d’abord le sérialiser en mémoire dans une String ou un MemoryStream, puis affecter cela au contenu de la requête :

async Task UploadJsonObjectAsync<T>(Uri uri, T data)
{
    var client = new HttpClient();
    string json = JsonConvert.SerializeObject(data);
    var response = await client.PostAsync(uri, new StringContent(json));
    response.EnsureSuccessStatusCode();
}

Ce n’est pas vraiment un problème pour des petits objets, mais ce n’est clairement pas optimal pour des graphes d’objets plus importants…

Alors, comment pourrait-on inverser ce modèle pull en un modèle push ? Eh bien c’est en fait assez simple : il suffit de créer une classe qui hérite de HttpContent, et de redéfinir sa méthode SerializeToStreamAsync pour écrire directement sur le flux de la requête. En fait, j’avais l’intention de bloguer sur ma propre implémentation, mais j’ai fait quelques recherches, et il s’avère que Microsoft a déjà fait le travail : la librairie Web API 2 Client fournit une classe PushStreamContent qui fait exactement ça. En gros, il faut juste lui passer un délégué qui définit quoi faire avec le flux de la requête. Voilà comment ça fonctionne :

async Task UploadJsonObjectAsync<T>(Uri uri, T data)
{
    var client = new HttpClient();
    var content = new PushStreamContent((stream, httpContent, transportContext) =>
    {
        var serializer = new JsonSerializer();
        using (var writer = new StreamWriter(stream))
        {
            serializer.Serialize(writer, data);
        }
    });
    var response = await client.PostAsync(uri, content);
    response.EnsureSuccessStatusCode();
}

Notez que la classe PushStreamContent fournit aussi une surcharge du constructeur qui accepte un délégué asynchrone, si vous voulez écrire sur le flux de façon asynchrone.

En fait, pour ce cas d’utilisation spécifique, la librairie Web API 2 Client propose une approche moins alambiquée : la classe ObjectContent. Il faut juste lui passer l’objet à envoyer ainsi qu’un MediaTypeFormatter, et elle se charge de sérialiser l’objet sur le flux de la requête :

async Task UploadJsonObjectAsync<T>(Uri uri, T data)
{
    var client = new HttpClient();
    var content = new ObjectContent<T>(data, new JsonMediaTypeFormatter());
    var response = await client.PostAsync(uri, content);
    response.EnsureSuccessStatusCode();
}

Par défaut, la classe JsonMediaTypeFormatter utilise Json.NET comme sérialiseur JSON, mais il y a une option pour utiliser DataContractJsonSerializer à la place.

Notez que si vous voulez lire un objet depuis le contenu de la réponse, c’est encore plus facile : utilisez simplement la méthode d’extension ReadAsAsync<T> (également dans la librairie Web API 2 Client). Comme vous pouvez le voir, il est donc extrêmement facile de consommer des APIs REST à l’aide de HttpClient.