iOS-HTTP-Retries prüfen: Wartezeiten, Limits und Abbruch

iOS-HTTP-Retries prüfen: Wartezeiten, Limits und Abbruch

iOS-HTTP-Retries prüfen: Wartezeiten, Limits und Abbruch

Die API antwortet mit 429, doch der Client sendet sofort drei weitere Anfragen. Der Nutzer hat sich bereits abgemeldet, aber eine alte Anfrage wird nach einer Wartezeit erneut gesendet. Solche Fehler lassen sich nicht durch längere Timeouts beheben. Wer die iOS-Netzwerkschicht auf einem Cloud-Mac abnimmt, sollte Wiederholungsversuche zunächst an drei klaren Grenzen prüfen: Wie wird die Antwort interpretiert, wann darf erneut gesendet werden, und wer verhindert das Senden nach einem Abbruch? Dafür müssen Tests nicht tatsächlich mehrere Dutzend Sekunden warten.

Zuerst festlegen, welche Anfragen wiederholt werden dürfen

Beginnen Sie mit rein lesenden GET-Anfragen. Das Beispiel behandelt ausschließlich 429 und 503. Für andere Statuscodes und Transportfehler sind eigene Strategien nötig; nicht jeder Fehler gehört in dieselbe Schleife. Die erste Übertragung zählt nicht als Wiederholungsversuch. Höchstens drei Wiederholungen bedeuten also höchstens vier Übertragungen.

Bei schreibenden Anfragen lässt sich die Sicherheit einer Wiederholung nicht allein am Methodennamen erkennen. Automatisches erneutes Senden kommt nur infrage, wenn der Server ein Idempotenzverfahren unterstützt, für denselben fachlichen Vorgang derselbe Idempotenzschlüssel verwendet wird und sich der Request-Body erneut lesen lässt. Bei Streaming-Uploads muss außerdem geprüft werden, ob sich der Stream neu erzeugen lässt.

Ein Timeout bedeutet nur, dass der Client kein Ergebnis erhalten hat. Es bedeutet nicht, dass der Server den Schreibvorgang nicht abgeschlossen hat. Auch der Abbruch einer lokalen Task macht einen Vorgang, der den Server bereits erreicht hat, nicht rückgängig.

Jede logische Anfrage benötigt ein eigenes Retry-Budget. Setzen Sie den Zähler nicht bei jedem Eintritt in die Netzwerkfunktion zurück. Wenn eine äußere und eine innere Schicht jeweils drei Wiederholungen ausführen, vervielfacht sich die tatsächliche Zahl der Anfragen.

Antwort-Header in verbindliche Wartevorgaben übersetzen

Retry-After erlaubt zwei Arten von Werten: eine nicht negative ganze Zahl von Sekunden oder ein HTTP-Datum. Bei Ganzzahlen werden ausschließlich ASCII-Ziffern akzeptiert; negative Zahlen, Dezimalzahlen und Werte mit Überlauf werden abgelehnt. Für die Datumsverarbeitung müssen ein englisches Gebietsschema und GMT fest vorgegeben werden, unabhängig von der aktuellen Sprache des Knotens. Das folgende Beispiel verarbeitet das übliche IMF-fixdate-Format:

import Foundation

func serverDelay(_ raw: String?, now: Date) -> TimeInterval? {
    guard let raw else { return nil }
    let text = raw.trimmingCharacters(in: .whitespacesAndNewlines)

    if !text.isEmpty,
       text.utf8.allSatisfy({ $0 >= 48 && $0 <= 57 }) {
        guard let seconds = UInt64(text) else { return nil }
        return TimeInterval(seconds)
    }

    let parser = DateFormatter()
    parser.locale = Locale(identifier: "en_US_POSIX")
    parser.timeZone = TimeZone(secondsFromGMT: 0)
    parser.dateFormat = "EEE, dd MMM yyyy HH:mm:ss 'GMT'"
    parser.isLenient = false

    guard let date = parser.date(from: text),
          parser.string(from: date) == text else {
        return nil
    }
    return max(0, date.timeIntervalSince(now))
}

Dieser Parser prüft das gängige Format strikt, unterstützt aber nicht sämtliche historischen HTTP-Datumsformate. Verlangt der API-Vertrag auch ältere Formate, brauchen diese eigene Parsing-Zweige und Test-Fixtures. Bei ungültigen Werten fällt das Beispiel auf den lokalen Backoff zurück und protokolliert den Grund des Parsing-Fehlers.

Fixieren Sie now und prüfen Sie getrennt Datumswerte in der Vergangenheit, in der Gegenwart und in der Zukunft. Die eigentliche Wartezeit wird mit einer monotonen Uhr gemessen. Die Wanduhr dient nur dazu, das Datum in verbleibende Sekunden umzurechnen, damit eine Korrektur der Systemzeit eine bereits laufende Wartezeit nicht verändert.

Backoff mit einer reinen Funktion berechnen, ohne darin zu warten

Speichern Sie den folgenden Inhalt als retry-check.swift, im Anschluss an die vorherige Parsing-Funktion. retryIndex beginnt bei null. Die Basisverzögerung beträgt nacheinander 1, 2 und 4 Sekunden; darauf wird proportionaler Jitter angewendet. Den Zufallswert liefert der Aufrufer.

enum Decision: Equatable {
    case stop
    case deferred
    case retry(TimeInterval)
}

func decide(
    status: Int,
    replayable: Bool,
    retryIndex: Int,
    server: TimeInterval?,
    unit: Double
) -> Decision {
    guard replayable,
          [429, 503].contains(status),
          (0..<3).contains(retryIndex),
          unit.isFinite,
          (0...1).contains(unit) else {
        return .stop
    }

    let base = pow(2.0, Double(retryIndex))
    let local = base * (0.5 + 0.5 * unit)

    if let server, !server.isFinite || server < 0 {
        return .stop
    }

    let delay = max(local, server ?? 0)
    guard delay <= 30 else { return .deferred }
    return .retry(delay)
}

precondition(decide(
    status: 503, replayable: true,
    retryIndex: 0, server: nil, unit: 0
) == .retry(0.5))

precondition(decide(
    status: 429, replayable: true,
    retryIndex: 1, server: 10, unit: 1
) == .retry(10))

precondition(decide(
    status: 429, replayable: true,
    retryIndex: 0, server: 60, unit: 0
) == .deferred)

precondition(decide(
    status: 503, replayable: true,
    retryIndex: 3, server: nil, unit: 1
) == .stop)

precondition(decide(
    status: 503, replayable: false,
    retryIndex: 0, server: nil, unit: 1
) == .stop)

Führen Sie die Datei auf einem Knoten mit eingerichteter Swift-Toolchain aus:

swift retry-check.swift

Ein reguläres Programmende bedeutet, dass die Prüfungen bestanden wurden. Verwenden Sie keine Optimierungsoptionen, die diese Prüfungen deaktivieren. Die 30 Sekunden sind hier eine beispielhafte Obergrenze für automatisches Warten, keine Protokollvorgabe. Verlangt der Server eine längere Wartezeit, wird der Status „aufgeschoben“ zurückgegeben. Die Wartezeit darf nicht auf 30 Sekunden gekürzt werden, um früher erneut zu senden.

Abbruchgrenzen prüfen, nicht nur Verzögerungswerte

Die Ausführungsschicht nimmt lediglich die Entscheidung entgegen und ruft anschließend eine injizierbare Wartefunktion auf. In der Produktion eignet sich ein abbrechbares Task.sleep. Tests verwenden einen kontrollierbaren Suspendierungspunkt mit einer Benachrichtigung, sobald die Wartephase erreicht ist. So muss der Test den Fortschritt der Task nicht anhand kurzer Schlafpausen erraten.

Prüfen Sie mindestens diese drei Abläufe:

Die Sitzungsgeneration ist eine Kennung, die bei jedem Wechsel des Anmeldestatus erhöht wird. Selbst wenn eine alte Anfrage erfolgreich beantwortet wird, muss vor dem Aktualisieren von Oberfläche oder Cache zuerst die Generation verglichen werden. Zwischen der Abbruchprüfung und dem tatsächlichen Senden bleibt eine Race Condition bestehen. Deshalb lässt sich nicht garantieren, dass nach einem Abbruch keinerlei Anfrage mehr beim Server ankommt.

Fehlernachweise in Merge-Prüfkriterien überführen

Ergänzen Sie die Parser-Tests um fehlende Werte, Leerraum, negative Zahlen, Dezimalzahlen, übergroße Ganzzahlen und Datumsgrenzfälle. Decken Sie beim Scheduler die Retry-Indizes null bis drei, die Zufallsgrenzwerte null und eins sowie Server-Wartezeiten genau an und oberhalb der Obergrenze ab.

Verwenden Sie in Integrationstests ein skriptgesteuertes Transport-Testdouble, das nacheinander 503, 429 und 200 zurückgibt. Prüfen Sie vor jeder Übertragung die geplante Verzögerung. Ergänzen Sie außerdem Szenarien für Abbruch, nicht erneut lesbare Request-Bodys und ein ausgeschöpftes Retry-Budget. Erzeugen Sie keine Drosselung über echte öffentliche APIs, und lassen Sie die Testumgebung nicht tatsächlich so lange warten, wie es der Antwort-Header vorgibt.

Protokollieren Sie ausschließlich die Kennung der logischen Anfrage, den Statuscode, den Retry-Index, die geplante Verzögerung und den Beendigungsgrund. Authentifizierungs-Header und Request-Bodys gehören nicht ins Log. Für die endgültige Abnahme zählen vier Punkte: kein vorzeitiges erneutes Senden, keine Übertragung über das Budget hinaus, keine Behandlung eines Abbruchs als gewöhnlicher Fehler und keine Wiederholungsschleife für nicht erneut sendbare Anfragen. Erst wenn das erfüllt ist, lohnt sich die Diskussion über angepasste Backoff-Parameter.

Häufig gestellte Fragen

Soll jeder Status 429 oder 503 einen Retry auslösen?

Nein. Zuerst müssen sichere Wiederholbarkeit, verbleibendes Budget und die Warteanforderung des Servers geprüft werden. Schreibzugriffe brauchen ein serverseitiges Idempotenzprotokoll und einen erneut sendbaren Body.

Darf Retry-After auf das eigene Wartelimit gekürzt werden?

Nein. Der nächste Request käme zu früh. Geben Sie einen Aufschub an die aufrufende Schicht zurück, die den Auftrag speichern oder den Nutzer informieren kann.

Wie lassen sich Backoff-Tests ohne echte Wartezeit ausführen?

Injizieren Sie Uhr, Zufallswert und Wartefunktion. Prüfen Sie berechnete Verzögerungen direkt und brechen Sie die Ausführung an einem kontrollierten Wartepunkt ab. Danach darf die Zahl der Sendungen nicht steigen.

Einen exklusiven Mac für deinen Build mieten?

Wähle die passende Mietdauer. Du erhältst einen kompletten physischen Mac exklusiv für dich – mit vollem Zugriff auf die macOS-Oberfläche und das Terminal. Modell, Standort und Verfügbarkeit zeigt die Konsole in Echtzeit an.

Nutze diese Schritte beim nächsten Archiv-Build.

Prüfe zuerst Mietdauer und Konfiguration, dann starte den Build.