HttpClient nicht mit using umschließen ── HTTP-Kommunikation in C#-Geschäftsanwendungen (Erzeugungsmuster, Timeouts, Retries)

· · CSharp, .NET, HttpClient, IHttpClientFactory, Netzwerk, async/await, Windows-Entwicklung, Technische Beratung

„Gegen Nachmittag beginnt die Verbindung zur externen API mit einer SocketException zu scheitern.“ „Obwohl das Ziel umgestellt wurde, verbindet sich die Anwendung weiterhin mit dem alten Server.“ – Der HttpClient in C# lässt sich mit einem einfachen Aufruf von GetAsync problemlos benutzen, aber wenn man Erzeugung und Haltung der Instanz falsch angeht, baut man genau diese Art von Fehler ein: Er funktioniert zunächst einwandfrei und bricht erst im laufenden Betrieb zusammen.

Dieser Artikel geht davon aus, dass eine Windows-Geschäftsanwendung externe Web-APIs oder interne Dienste aufruft, und ordnet die richtigen Erzeugungsmuster für HttpClient, das Timeout-Design, Retries, Fehlerbehandlung sowie Windows-spezifische Fallstricke in der Reihenfolge, in der sie in der Praxis typischerweise zu Entscheidungen zwingen.

1. Zuerst das Ergebnis (Entscheidungstabelle)

Wie HttpClient gehalten werden sollte, hängt vom Aufbau der Anwendung ab. Zunächst die Entscheidungstabelle im Überblick.

Art der Anwendung Empfohlenes Muster Begründung
Konsolen-Tool (.NET), das nach Sekunden bis Minuten endet Ein einziger static/Singleton-HttpClient Bei kurzlebigen Prozessen kann das DNS-Änderungsproblem praktisch ignoriert werden
Dauerhaft laufende Anwendung / Windows-Dienst (.NET, ohne DI) static-HttpClient + SocketsHttpHandler.PooledConnectionLifetime-Einstellung Löst sowohl Socket-Erschöpfung als auch das DNS-Änderungsproblem
Anwendung mit Generic Host / DI (.NET) IHttpClientFactory (AddHttpClient) Pooling und Austausch der Handler werden der Factory überlassen. Mit benannten/typisierten Clients lassen sich Einstellungen pro Ziel trennen
Anwendung im .NET Framework Einführung von IHttpClientFactory über das Paket Microsoft.Extensions.Http Im .NET Framework kommt es bei selbst verwalteter Erzeugung leicht zu Port-Erschöpfung; auch offiziell wird die Verwendung der Factory empfohlen1
Proxy-, Cookie- oder Zertifikatseinstellungen unterscheiden sich je nach Ziel Für jede Konfiguration einen eigenen HttpClient verwenden (nicht wiederverwenden) Die Verbindungseinstellungen des Handlers lassen sich nach dem Versand der ersten Anfrage nicht mehr ändern2

Vorab das Fazit:

  • Erzeugen Sie bei jeder Anfrage kein neues new HttpClient(), das Sie mit using verwerfen. HttpClient verwaltet intern einen Verbindungspool und ist auf Wiederverwendung ausgelegt. Werden bei jeder Anfrage neue Instanzen erzeugt und verworfen, gehen unter hoher Last die verfügbaren Sockets aus und es kommt zu einer SocketException2.
  • Ein einfaches static ist aber auch keine Komplettlösung. HttpClient löst DNS nur beim Aufbau einer Verbindung auf, sodass er auch bei geänderter IP-Adresse des Ziels weiterhin die alte Verbindung nutzt. Die offiziell empfohlene Lösung ist, die Lebensdauer der Verbindungen über SocketsHttpHandler.PooledConnectionLifetime zu begrenzen1.
  • Bei Generic Host oder DI überlassen Sie das IHttpClientFactory. Die Factory poolt die Handler und tauscht sie standardmäßig alle 2 Minuten aus, wodurch sowohl Socket-Erschöpfung als auch DNS-Änderungen abgedeckt werden3.
  • Der Standard-Timeout beträgt 100 Sekunden. Für den gefühlten Nutzen in einer Geschäftsanwendung ist das kaum von einem „unendlichen Hängenbleiben“ zu unterscheiden – legen Sie den Wert daher pro Ziel explizit fest.
  • Bauen Sie Retries nicht selbst; setzen Sie auf den Standard-Handler aus Microsoft.Extensions.Http.Resilience. Damit lassen sich Retry, Circuit Breaker und Timeout als bewährtes Standardpaket einführen, und typische Fehler selbst gebauter Retry-Schleifen – etwa das bedingungslose erneute Senden eines fehlgeschlagenen POST mit doppelter Registrierung als Folge – werden bereits im Design vermieden4.

2. Warum „bei jeder Anfrage mit using erzeugen“ ein Problem ist – Socket-Erschöpfung

Da HttpClient IDisposable implementiert, wirkt folgender Code auf den ersten Blick korrekt.

// Anti-Pattern: bei jeder Anfrage neu erzeugen und wieder verwerfen
public async Task<string> GetDataAsync(string url)
{
    using var client = new HttpClient();
    return await client.GetStringAsync(url);
}

Das Problem: Auch nach Dispose wird der Socket auf Betriebssystemebene nicht sofort freigegeben. Gemäß TCP-Spezifikation verbleibt der schließende Socket noch eine Weile im Zustand TIME_WAIT. Solange die Aufrufhäufigkeit gering ist, passiert nichts – steigt die Last, sammeln sich jedoch nicht freigegebene Sockets an, bis irgendwann plötzlich eine SocketException auftritt und keine Verbindung mehr möglich ist2.

Tückisch an diesem Fehler ist, dass er sich während Entwicklung und Test kaum reproduzieren lässt. Er tritt typischerweise nur zur Hauptlastzeit im Produktivbetrieb oder beim Monatsend-Batch auf. Bei der Untersuchung liefert folgender Befehl während des Auftretens einen Hinweis, indem er die Anzahl der TIME_WAIT-Sockets pro Ziel zählt.

# Anzahl der TIME_WAIT-Sockets pro Ziel zusammenfassen
netstat -ano | Select-String "TIME_WAIT" | Measure-Object

Ein über IHttpClientFactory bezogener HttpClient ist von diesem Problem nicht betroffen. Bei einem von der Factory erzeugten Client wird beim Dispose nur der Client selbst, nicht aber der Handler (der eigentliche Verbindungspool) verworfen – ein Umschließen mit using ist daher unbedenklich3.

3. Ein statisches HttpClient ist auch keine Komplettlösung – das DNS-Änderungsproblem

Um Socket-Erschöpfung zu vermeiden, HttpClient als static zu deklarieren, ist der richtige Ansatz – löst aber allein nicht alle Probleme. HttpClient löst DNS nur beim Aufbau einer Verbindung auf und berücksichtigt dabei auch nicht die TTL der DNS-Einträge2. Solange eine Verbindung im Pool bestehen bleibt, wird trotz geänderter IP-Adresse des Ziels weiterhin die alte IP-Adresse angesprochen.

Der Ausfall „Nach dem DNS-Failover wurde bis zum Neustart weiterhin der alte Server angesprochen“ hat genau diese Ursache. Die von den offiziellen Richtlinien empfohlene Lösung besteht darin, die Lebensdauer der Verbindungen über SocketsHttpHandler.PooledConnectionLifetime zu begrenzen1.

// Empfohlenes Muster für .NET (Core) / .NET 5+:
// Verbindungen werden periodisch neu aufgebaut und folgen so DNS-Änderungen
private static readonly HttpClient SharedClient = new(new SocketsHttpHandler
{
    PooledConnectionLifetime = TimeSpan.FromMinutes(2)
});

Eine abgelaufene Verbindung wird bei der nächsten Anfrage neu aufgebaut, wobei DNS erneut aufgelöst wird. Der Wert richtet sich danach, wie schnell DNS-Änderungen übernommen werden sollen. In den offiziellen Beispielen werden 2 Minuten verwendet; bei internen Systemen, deren Ziel sich selten ändert, kann der Wert problemlos höher gewählt werden1.

SocketsHttpHandler ist eine Implementierung ab .NET Core 2.1 und im .NET Framework nicht verfügbar. Verwenden Sie im .NET Framework das im nächsten Abschnitt beschriebene IHttpClientFactory1.

4. Bei Verwendung von DI: IHttpClientFactory

Für Anwendungen, die Generic Host oder einen DI-Container verwenden, ist IHttpClientFactory (AddHttpClient) die erste Wahl. Eine Erklärung zu Generic Host selbst finden Sie unter „Was ist der Generic Host?“, zur Einführung in Desktop-Anwendungen unter „Generic Host + BackgroundService in einer Desktop-Anwendung einsetzen“.

using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;

HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);

// Benannter Client: Einstellungen pro Ziel trennen
builder.Services.AddHttpClient("OrderApi", client =>
{
    client.BaseAddress = new Uri("https://order.example.co.jp/");
    client.Timeout = TimeSpan.FromSeconds(10);
});

Beim Verhalten der Factory sind drei Punkte wichtig:

  • Handler werden gepoolt und standardmäßig alle 2 Minuten ausgetauscht. Bei jedem Aufruf von CreateClient wird zwar ein neuer HttpClient zurückgegeben, der dahinterliegende Handler (Verbindungspool) wird jedoch geteilt – Socket-Erschöpfung entsteht dadurch nicht, und der regelmäßige Austausch sorgt dafür, dass DNS-Änderungen ebenfalls berücksichtigt werden3.
  • Ein von der Factory erzeugter HttpClient ist für kurzlebige Verwendung gedacht. Wird die erhaltene Instanz in einem Singleton-Feld gehalten, kann sie am Austausch der Handler nicht mehr teilnehmen und folgt DNS-Änderungen nicht mehr. Aus demselben Grund sollten Sie es vermeiden, einen typisierten Client in einen Singleton-Dienst zu injizieren3.
  • Bei Anwendungen, die auf Cookies angewiesen sind, ist Vorsicht geboten. Da Handler gepoolt werden, wird auch der CookieContainer unbeabsichtigt geteilt. Wenn Cookies benötigt werden, sollte entweder die Factory vermieden oder die Cookie-Verarbeitung deaktiviert und Header selbst gesetzt werden – so wird es auch offiziell empfohlen1.

Für den Umgang mit Tokens beim Aufruf authentifizierter APIs (etwa durch Microsoft Entra ID geschützte APIs) siehe „Entra-ID-Authentifizierung in WinForms/WPF-Anwendungen integrieren“.

5. Timeout-Design – der Standardwert von 100 Sekunden ist für Geschäftsanwendungen zu lang

Der Standardwert von HttpClient.Timeout beträgt 100 Sekunden5. In einer Geschäftsanwendung, in der eine API im direkten Anschluss an eine Bildschirmaktion aufgerufen wird, wirkt eine Wartezeit von 100 Sekunden wie ein Einfrieren der Anwendung – legen Sie den Wert daher pro Ziel explizit fest.

// Standard-Timeout für den gesamten Client
client.Timeout = TimeSpan.FromSeconds(10);

// Soll nur eine bestimmte Anfrage kürzer/länger sein, CancellationTokenSource verwenden
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(3));
HttpResponseMessage response = await client.GetAsync(url, cts.Token);

Beim Design sind folgende Punkte zu beachten:

  • Bei einem Timeout wird eine TaskCanceledException ausgelöst. Ab .NET 5 enthält diese bei einem durch HttpClient.Timeout verursachten Timeout eine TimeoutException als innere Exception5. Bei einem Timeout über eine eigene CancellationTokenSource wie oben ist dagegen keine innere Exception gesetzt. Um Timeout und eine vom Aufrufer ausgelöste Cancellation zuverlässig zu unterscheiden, sollte man nicht die innere Exception prüfen, sondern ob das vom Aufrufer übergebene Token bereits als „cancelled“ markiert ist (siehe Codebeispiel in Abschnitt 7). Achten Sie darauf: Wer im catch-Block nur HttpRequestException abfängt, übersieht Timeouts.
  • Timeout begrenzt die „gesamte Anfrage“. Soll nur der Verbindungsaufbau kurz begrenzt werden, kombinieren Sie zusätzlich SocketsHttpHandler.ConnectTimeout. Eine Anforderung wie „Wenn der Server nicht erreichbar ist, nach 3 Sekunden aufgeben, im Normalfall aber bei großen Antworten bis zu 60 Sekunden warten“ lässt sich mit dieser Kombination abbilden2.
  • Beim Herunterladen großer Dateien sollte die Standard-Pufferung vermieden werden. HttpClient liest die gesamte Antwort standardmäßig in den Arbeitsspeicher – bei Downloads ab mehreren Zehn-MB sollte daher HttpCompletionOption.ResponseHeadersRead angegeben und die Antwort als Stream verarbeitet werden2.

Wenn Timeout-Werte in appsettings.json ausgelagert und pro Umgebung angepasst werden sollen, lässt sich die Entscheidungstabelle aus „Praxisleitfaden für die Konfigurationsverwaltung in Windows-Geschäftsanwendungen“ direkt übernehmen.

Wird HTTP-Kommunikation aus WinForms/WPF heraus aufgerufen und dabei mit .Result oder .Wait() blockiert, führt das zu einem Deadlock des UI-Threads. Dieser klassische Fallstrick wird ausführlich in „Entscheidungstabelle für C# async/await in der Praxis“ erläutert – eine Lektüre lohnt sich, bevor Sie Kommunikationscode schreiben.

6. Retries – standardmäßiger Resilience-Handler statt selbst gebauter Schleife

Da Netzwerkverbindungen gelegentlich vorübergehend fehlschlagen, benötigen Aufrufe externer APIs Retries. Eine selbst gebaute Retry-Logik mit for-Schleife und Task.Delay erfordert jedoch, dass alle folgenden Punkte selbst korrekt umgesetzt werden – der Aufwand lohnt sich kaum:

  • Unterscheidung zwischen Fehlern, bei denen ein Retry sinnvoll ist (Timeout, HTTP 408/429/5xx), und solchen, bei denen ein Retry nutzlos ist (HTTP 400/401/404)
  • Ausschluss von HTTP-Methoden, deren erneute Ausführung ein Problem verursachen kann (etwa doppelte Ausführung einer Registrierung per POST)
  • Exponentielles Backoff mit Jitter (Streuung, die verhindert, dass alle Clients gleichzeitig erneut senden und so den Server erneut überlasten)
  • Ein Circuit Breaker, der Aufrufe bei anhaltenden Störungen ganz stoppt

Der Standard-Handler des Pakets Microsoft.Extensions.Http.Resilience stellt dieses gesamte Paket mit bewährten Standardwerten bereit4.

builder.Services.AddHttpClient("OrderApi", client =>
    {
        client.BaseAddress = new Uri("https://order.example.co.jp/");
    })
    .AddStandardResilienceHandler(); // Standardpaket aus Retry + Circuit Breaker + Timeout

Die Standardwerte des Standard-Handlers umfassen ein Gesamt-Timeout von 30 Sekunden pro Anfrage, maximal 3 exponentielle Backoff-Retries (anfängliche Verzögerung 2 Sekunden, mit Jitter), ein Timeout von 10 Sekunden pro Versuch sowie die Behandlung von HTTP 408/429/5xx und HttpRequestException als vorübergehende Fehler4.

Ein wichtiger Punkt bei den Standardwerten: Der Standard-Handler wiederholt standardmäßig alle HTTP-Methoden. Bei APIs, bei denen eine doppelte Ausführung eines registrierenden POST ein Problem darstellt, sollten Sie Retries für unsichere Methoden deaktivieren4.

httpClientBuilder.AddStandardResilienceHandler(options =>
{
    // Erneute Ausführung von POST/PUT/DELETE usw. deaktivieren
    options.Retry.DisableForUnsafeHttpMethods();
});

Retries lösen jedoch ausschließlich vorübergehende Fehler. Symptome wie eine bestehende Verbindung, über die aber keine Daten fließen, oder extrem langsame Antworten sind oft ein Problem der TCP-Schicht; für die Eingrenzung solcher Fälle ist die in „TCP-Retransmission und RFC 1323 – Untersuchung eines Kommunikationsausfalls bei einer Industriekamera“ beschriebene Methode hilfreich.

7. Fehlerbehandlung – der Umgang mit Statuscodes

HttpClient löst bei einer „auf HTTP-Ebene fehlgeschlagenen, aber dennoch beantworteten“ Anfrage – etwa HTTP 404 oder 500 – keine Exception aus. Eine Exception entsteht nur, wenn gar keine Antwort zustande kommt, etwa bei Verbindungsfehler, Timeout oder Cancellation. Diese beiden Fälle sollten bewusst getrennt behandelt werden.

try
{
    using HttpResponseMessage response = await client.GetAsync(url, ct);

    if (!response.IsSuccessStatusCode)
    {
        // Eine Antwort kam zurück, war aber ein Fehlschlag: anhand des Statuscodes verzweigen
        if (response.StatusCode == HttpStatusCode.NotFound)
        {
            return null; // Beispiel: "nicht vorhanden" als Normalfall behandeln
        }
        response.EnsureSuccessStatusCode(); // alles andere führt zu HttpRequestException
    }

    return await response.Content.ReadFromJsonAsync<Order>(ct);
}
catch (HttpRequestException ex)
{
    // Verbindungsfehler oder Fehlschlag durch EnsureSuccessStatusCode.
    // Ab .NET 5 lässt sich der Statuscode des Fehlschlags über ex.StatusCode abrufen
    logger.LogError(ex, "Aufruf der Order-API fehlgeschlagen. StatusCode={StatusCode}", ex.StatusCode);
    throw;
}
catch (TaskCanceledException) when (ct.IsCancellationRequested)
{
    // Cancellation über das vom Aufrufer übergebene Token (z. B. Fenster geschlossen).
    // Kein Fehler, daher ohne Logeintrag einfach weiterreichen
    throw;
}
catch (TaskCanceledException ex)
{
    // Ablauf von HttpClient.Timeout oder einer eigenen Timeout-CTS
    logger.LogError(ex, "Aufruf der Order-API hat das Timeout überschritten");
    throw;
}

Ob 404 als Exception oder als null behandelt wird, hängt von der Semantik der aufgerufenen API ab. Wird pauschal alles über EnsureSuccessStatusCode in eine Exception verwandelt, wachsen die catch-Blöcke auf Aufruferseite unnötig an. Die Abgrenzung, was zur Exception wird und was über einen Rückgabewert ausgedrückt wird, lässt sich direkt aus dem Ansatz in „catch, Logging und Fehlerbehandlung von Exceptions in der Praxis“ übernehmen.

Für den JSON-Austausch empfiehlt sich GetFromJsonAsync / PostAsJsonAsync / ReadFromJsonAsync aus System.Net.Http.Json – dann muss keine Serialisierung über Zwischenstrings selbst geschrieben werden.

8. Windows-spezifische Fallstricke in Geschäftsanwendungen

Zum Abschluss eine Zusammenstellung typischer Fallstricke im Windows-Umfeld.

  • Die erste Anfrage ist wegen automatischer Proxy-Erkennung langsam. Unter Windows verwendet HttpClient standardmäßig die Proxy-Einstellungen des Betriebssystems (einschließlich automatischer Erkennung). Ist bekannt, dass kein Proxy benötigt wird, entfällt die Wartezeit für die Erkennung durch Deaktivieren über HttpClientHandler.UseProxy = false2. In Umgebungen mit zwingend erforderlichem internen Proxy verhindert dagegen eine explizite Angabe über WebProxy das Problem „funktioniert auf der Entwicklungsmaschine, aber nicht auf dem Server“.
  • Proxy-Einstellungen müssen vor der ersten Anfrage abgeschlossen sein. Verbindungsbezogene Einstellungen des Handlers werden nach dem Versand der ersten Anfrage nicht mehr wirksam, selbst wenn sie danach geändert werden2.
  • Die Standardwerte für die Anzahl gleichzeitiger Verbindungen sind in .NET und .NET Framework genau entgegengesetzt. In .NET (SocketsHttpHandler) ist die Anzahl gleichzeitiger HTTP/1.1-Verbindungen standardmäßig unbegrenzt, sodass bei sehr vielen parallelen Anfragen die Verbindungsanzahl immer weiter steigt und an Grenzen von Firewalls oder Servern stoßen kann. Setzen Sie bei Prozessen mit hoher Parallelität ein Limit über MaxConnectionsPerServer2. Im .NET Framework ist dagegen der Standardwert von ServicePointManager.DefaultConnectionLimit mit 2 (außerhalb von ASP.NET-Hosting) sehr klein, sodass parallele Anfragen intern warten müssen und dabei in ein Timeout laufen können. Wenn Sie im .NET Framework die Parallelität erhöhen möchten, müssen Sie dieses Limit explizit anheben6.
  • Beim Aufruf aus einem Windows-Dienst unterscheidet sich der Kontext für Proxy und TLS vom interaktiven Benutzer. Das Ausführungskonto des Dienstes verfügt nicht über die Proxy-Einstellungen oder Anmeldeinformationen des angemeldeten Benutzers – eine typische Ursache für Kommunikationsprobleme nach dem Muster „funktioniert im interaktiven Benutzerkontext, aber nicht als Dienst“. Zum dienstspezifischen Ausführungskontext siehe „Windows-Dienste erstellen und betreiben“.
  • URLs und API-Schlüssel nicht im Code hinterlegen. Für die Umstellung des Kommunikationsziels über Konfigurationsdateien siehe „Praxisleitfaden für die Konfigurationsverwaltung“, für die Speicherung geheimer Informationen „Klartext-Konfigurationen mit DPAPI vermeiden“.

Zusammenfassung

Bei HttpClient entscheidet in der Praxis weniger das „Wie ruft man auf“ über die Qualität als das „Wie hält man die Instanz“. Eine Erzeugung pro Anfrage führt zu Socket-Erschöpfung, ein unbedarftes static dazu, dass DNS-Änderungen nicht mehr berücksichtigt werden – beides bleibt während der Entwicklung unsichtbar. Für .NET ist die Antwort eine geteilte Instanz mit PooledConnectionLifetime oder IHttpClientFactory, für das .NET Framework die Einführung von IHttpClientFactory. Legen Sie darüber hinaus Timeouts pro Ziel explizit fest, und überlassen Sie Retries dem Standard-Resilience-Handler – erst dann entsteht eine Geschäftsanwendung, die ein „gelegentlich fehlschlagendes Netzwerk“ als Normalfall einplant.

Die Überarbeitung der Kommunikation in einer bestehenden Anwendung (Verbindungsabbrüche nur zu Spitzenlastzeiten, Neuordnung des Timeout-Designs, Neuentwicklung einer externen API-Anbindung) erfordert häufig eine Beurteilung anhand des tatsächlichen Codes und der Betriebsumgebung – sprechen Sie uns bei Unsicherheiten gerne an.

Verwandte Artikel

Verwandte Beratungsbereiche

Komura Soft LLC bietet die Entwicklung von Windows-Geschäftsanwendungen einschließlich externer API-Anbindung sowie technische Beratung zur Untersuchung und Behebung von Kommunikationsproblemen in bestehenden Anwendungen (Socket-Erschöpfung, Timeouts, sporadische Verbindungsfehler) an.

Quellen

  1. Microsoft Learn, Guidelines for using HttpClient. Behandelt: Für .NET Core/.NET 5+ entweder eine langlebige Instanz mit PooledConnectionLifetime oder eine kurzlebige, von IHttpClientFactory erzeugte Instanz verwenden; im .NET Framework wird die Verwendung von IHttpClientFactory empfohlen; Anwendungen, die Cookies verwenden, sollten IHttpClientFactory wegen der gemeinsamen Nutzung des CookieContainer vermeiden.  2 3 4 5 6

  2. Microsoft Learn, HttpClient Class. Behandelt: Erzeugung pro Anfrage führt zu Socket-Erschöpfung und SocketException; DNS wird nur beim Verbindungsaufbau aufgelöst, TTL wird nicht berücksichtigt; Verbindungseinstellungen des Handlers lassen sich nach der ersten Anfrage nicht mehr ändern; die Anzahl gleichzeitiger HTTP/1.1-Verbindungen ist standardmäßig unbegrenzt; Empfehlung zum Streaming bei großen Downloads; Standardverhalten bei Proxys und Deaktivierung über UseProxy 2 3 4 5 6 7 8 9

  3. Microsoft Learn, IHttpClientFactory with .NET. Behandelt: Standardlebensdauer der Handler von 2 Minuten; von der Factory erzeugte HttpClient-Instanzen sind für kurzlebige Verwendung gedacht; Dispose einer von der Factory erzeugten Instanz verwirft den Handler nicht; die Injektion eines typisierten Clients in einen Singleton führt dazu, dass DNS-Änderungen nicht mehr berücksichtigt werden.  2 3 4

  4. Microsoft Learn, Build resilient HTTP apps: Key development patterns. Behandelt: die von AddStandardResilienceHandler konfigurierten fünf Strategien (Rate Limiter / Gesamt-Timeout 30 Sekunden / maximal 3 exponentielle Backoff-Retries / Circuit Breaker / Timeout pro Versuch 10 Sekunden); die betroffenen Statuscodes (408/429/5xx) und Exceptions; die Deaktivierung von Retries für POST usw. über DisableForUnsafeHttpMethods 2 3 4

  5. Microsoft Learn, HttpClient.Timeout Property. Behandelt: Standardwert von 100 Sekunden; ab .NET 5 wird bei Timeout eine TaskCanceledException mit einer TimeoutException als innerer Exception ausgelöst.  2

  6. Microsoft Learn, ServicePointManager.DefaultConnectionLimit Property. Behandelt: Der Standardwert für die Anzahl gleichzeitiger Verbindungen beträgt 10 bei in ASP.NET gehosteten Anwendungen und 2 bei anderen (z. B. Desktop-Anwendungen). 

Aktuelle Artikel mit denselben Schlagwörtern führen zu verwandten Themen weiter.

Diese Seiten ordnen den Artikel in einen größeren Leistungs- und Entscheidungskontext ein.

Dieser Artikel ist direkt mit den folgenden Leistungen verbunden.

Häufige Fragen

Fragen, die in Beratungen zu diesem Artikelthema häufig gestellt werden.

HttpClient implementiert IDisposable – darf man ihn trotzdem nicht mit using umschließen?
Problematisch ist die Praxis, bei jeder Anfrage eine neue Instanz zu erzeugen und sie danach zu verwerfen. HttpClient verwaltet intern einen Verbindungspool und ist darauf ausgelegt, über die gesamte Lebensdauer der Anwendung wiederverwendet zu werden. Wird er bei jeder Anfrage neu erzeugt und verworfen, bleiben die zugehörigen Sockets nach dem Dispose noch eine Weile im Zustand TIME_WAIT bestehen – unter hoher Last gehen so die verfügbaren Sockets aus, und es kommt zu einer SocketException. Verwerfen Sie die Instanz stattdessen nur einmal beim Beenden der Anwendung, oder beziehen Sie sie über IHttpClientFactory. Ein über die Factory bezogener HttpClient darf bedenkenlos mit using umschlossen werden, da dabei nur der Client, nicht aber der zugrunde liegende Handler verworfen wird.
Sollte man von WebClient oder HttpWebRequest aus dem .NET Framework migrieren?
Für neuen Code empfiehlt sich durchgehend HttpClient. WebClient und HttpWebRequest sind ältere APIs, die nur aus Kompatibilitätsgründen erhalten geblieben sind; auch Microsoft empfiehlt für Neuentwicklungen HttpClient. Ein vollständiges Ersetzen im Bestandscode ist nicht zwingend nötig, aber es lohnt sich, bei Änderungen an der Kommunikationslogik nach und nach auf HttpClient umzustellen – das erleichtert Timeout-Steuerung, async-Unterstützung und Testbarkeit.
Wie oft und in welchen Abständen sollte man Retries durchführen?
Statt eigene Werte festzulegen, ist es sicherer, von den Standardwerten des Standard-Handlers aus Microsoft.Extensions.Http.Resilience auszugehen (maximal 3 Versuche, exponentielles Backoff mit Jitter, anfängliche Verzögerung 2 Sekunden). Wichtiger als die Anzahl ist die Unterscheidung, ob eine Anfrage überhaupt wiederholt werden darf: Operationen wie POST, bei denen eine erneute Ausführung zu einer Doppelbuchung führen kann, sollten standardmäßig von Retries ausgeschlossen werden – oder Retries erst aktiviert werden, nachdem serverseitig Idempotenz sichergestellt ist (dieselbe Anfrage führt bei zweimaligem Empfang zum gleichen Ergebnis).
Warum ist in einer internen Proxy-Umgebung ausgerechnet die erste Anfrage extrem langsam?
In der Windows-Standardkonfiguration versucht HttpClient, den Proxy automatisch zu erkennen, was bei der ersten Verbindung Zeit kosten kann. In Umgebungen, in denen sicher kein Proxy benötigt wird (etwa bei interner Server-zu-Server-Kommunikation), lässt sich das durch Deaktivieren der automatischen Erkennung über HttpClientHandler.UseProxy = false verbessern. Umgekehrt ist in internen Umgebungen mit Proxy-Zwang eine explizite Angabe über WebProxy stabiler als sich auf die automatische Erkennung zu verlassen.

Autorenprofil

Profilseite des Artikelautors.

Go Komura

Geschäftsführer von KomuraSoft LLC

Spezialisiert auf Windows-Softwareentwicklung, technische Beratung und Fehleranalyse, insbesondere bei bestehenden Systemen und schwer reproduzierbaren Störungen.

Öffentliche Links

Zurück zum Blog