HTTP Timeouts in Go

| Foto von Kenny Eliason auf Unsplash

HTTP Timeouts in Golang

Was sind Timeouts und wofür brauche ich sie?

Informell ist ein Timeout eine bestimmte Zeitspanne, die ein Ereignis dauern darf. Wenn diese Zeitspanne abläuft, reagiert das ausführende System auf das Ablaufen dieser Zeitspanne - meistens ist das das Beenden des Ereignisses. Ein Timeout kann durch bestimmte Aktionen erneuert werden. Ein einfaches Beispiel: Das Display meines Smartphones schaltet sich nach einigen Minuten automatisch aus. In den Einstellungen kann ich diese Zeitspanne einstellen. Durch das Berühren meines Bildschirms setze ich die Zeit bis zum Ausschalten wieder zurück. Dieser Timeout existiert vor allem, damit mein Smartphone durch das Display bei Inaktivität nicht zu viel Akku verbraucht.

Alle (relevanten) Timeouts im http.Client

Für die Implementierung von HTTP Clients besitzt Go eine Menge einstellbarer Timeouts. Zum Glück macht Go es uns relativ einfach und bietet eine Standardimplementierung für Clients an. Wir können also einen HTTP Client mit allen Standardwerten bauen.

    client := http.DefaultClient
    fmt.Println(client.Timeout)

Suchen wir in unserem Client nach Timeouts, scheint es aber nur einen zu geben. Wenn wir uns diesen ausgeben lassen, scheint er einen Standardwert von 0 zu haben. In den meisten Fällen ist es aber sehr zu empfehlen ein Timeout einzustellen.

Der DefaultClient wird für die Wrapperfunktionen wie http.Get oder http.Post benutzt! Wollen wir also ein HTTP Request mit Timeout durchführen, müssen wir vorher einen Client erstellen, den Timeout setzen und dann client.Get() nutzen. Wichtig also zu wissen, dass wir dem DefaultClient einen Timeout mitgeben sollten. Der Timeout des Go HTTP Clients ist eine Art Gesamttimeout und bestimmt die Zeitspanne, die der HTTP Client brauchen darf, um seine Verbindung aufzubauen, Daten zu senden, eine Antwort (Response) zu bekommen und die Daten aus der Antwort zu lesen. Überschreitet eine Anfrage (Request) den Timeout, wird sie mit einem Timeoutfehler abgebrochen.

Go’s HTTP Client besitzt ein struct Transport. Dieses struct beschreibt die Art und Weise, wie ein HTTP Request zusammengesetzt wird. Der Standardclient nutzt einen DefaultTransport, welcher folgende Timeouts setzt:

var DefaultTransport RoundTripper = &Transport{
    Proxy: ProxyFromEnvironment,
    DialContext: defaultTransportDialContext(&net.Dialer{
        Timeout:   30 * time.Second,
        KeepAlive: 30 * time.Second,
    }),
    ForceAttemptHTTP2:     true,
    MaxIdleConns:          100,
    IdleConnTimeout:       90 * time.Second,
    TLSHandshakeTimeout:   10 * time.Second,
    ExpectContinueTimeout: 1 * time.Second,
}

DialContext Timeout

DialContext beinhaltet Optionen zum Verbinden mit einer bestimmten Netzwerkadresse. Timeout beschränkt die Zeit, die vergehen darf, um eine Verbindung mit dieser Adresse aufzubauen.

DialContext KeepAlive

Das ist das Zeitintervall, in der der Golang Client sogenannte Keep-Alive Anfragen sendet. Diese sind ein Mechanismus zum Überprüfen, ob eine Verbindung noch aktiv ist. HTTP Verbindungen können offen gehalten werden. Wir können eine Verbindung also wiederverwenden, sollten wir mehrere Anfragen senden wollen.

MaxIdleConns und IdleConnTimeout

IdleConnTimeout beschreibt die Zeit, die eine (KeepAlive) Verbindung offen bleiben kann solange auf ihr nichts passiert (Idle = Leerlauf). MaxIdleConns bestimmt die Anzahl an Verbindungen im Leerlauf die ein Client behalten darf. Kommt eine neue Verbindung dazu, wird die Verbindung, welche am längsten im Leerlauf ist, entfernt. Zusätzlich ist es noch möglich MaxIdleConnsPerHost zu setzen, um die Verbindungen im Leerlauf einzuschränken, die ein Client zu einem Host nutzen kann.

TLSHandshakeTimeout

Dieser Timeout bestimmt die Zeit, die auf den TLS Handshake gewartet wird.

ExpectContinueTimeout

Der HTTP Status 100 Continue kann von unserem Client in der Anfrage angefordert werden. Wenn der Empfänger diesen Status sendet, wissen wir, dass wir weitere Daten senden können. Der ExpectContinueTimeout schränkt die Zeit (abzüglich der Zeit zum Senden der Anfrage) ein, die wir auf diese Antwort warten.

ResponseHeaderTimeout

Der ResponseHeader findet sich im DefaultTransport nicht, ist aber im Transport konfigurierbar und schränkt die Zeit ein, die wir nach Senden unserer Anfrage auf die Header der Antwort warten.

Timeouts Einstellen am Beispiel

Ein Beispiel: Nehmen wir an wir nutzen unseren Client zum Herunterladen von größeren Dateien aus dem Internet. Wie konfigurieren wir also unseren HTTP Client?

    transport := http.Transport{
        Proxy: http.ProxyFromEnvironment,
        DialContext: (&net.Dialer{
            Timeout:   30 * time.Second,
            KeepAlive: 30 * time.Second,
        }).DialContext,
        MaxIdleConns:          100,
        MaxIdleConnsPerHost:   10,
        IdleConnTimeout:       90 * time.Second,
        TLSHandshakeTimeout:   10 * time.Second,
        ResponseHeaderTimeout: 10 * time.Second,
        ExpectContinueTimeout: 10 * time.Second,
    }
    client := &http.Client{
        Transport: &transport,
        Timeout:   3 * time.Hour,
    }
  1. Wir geben dem DialContext einen leicht erhöhten Timeout von 30 Sekunden. Eine Keep-Alive Anfrage wird ebenfalls alle 30 Sekunden gesendet.
  2. Um zu ermöglichen, dass wir mehrere Dateien gleichzeitig von einem Host herunterladen können, müssen wir die maximalen Leerlaufverbindungen pro Host (MaxIdleConnsPerHost) hochsetzen. Die maximale Anzahl an IdleConnections lassen wir erstmal unverändert, da es bei 100 keine Unterschiede machen sollte.
  3. ResponseHeaderTimeout und ExpectContinueTimeout setzen wir etwas höher als im DefaulTransport, da wir etwas mehr Toleranz für langsame Verbindungen einrechnen wollen.
  4. Den Timeout des HTTP Clients setzen wir auf 3 Stunden. Das ist eine Menge Zeit, aber wir wollen selbst mit extrem langsamen Internetverbindungen noch in der Lage sein, große Dateien herunterzuladen. Unser Richtwert dafür ist eine 1GB große Datei bei einer 1Mbit/s Verbindung. Dadurch, dass wir unsere KeepAlive aber alle 30 Sekunden abfragen, schließen wir nach spätestens 30 Sekunden Verbindungen, die sich im Leerlauf befinden.

Wichtige Notizen

  1. Golangs Default HTTP Client (http.client) setzt keinen Timeout - daher lohnt es sich in den meisten Fällen einen eigenen Client zu erstellen.
  2. Der DefaultTransport kann genutzt werden, wenn man eine schnelle Konfiguration der feineren Timeouts braucht und keine besonderen Anforderungen beachten muss.