Vérifier l’intégrité et la taille décompressée des réponses gzip sur iOS

Vérifier l’intégrité et la taille décompressée des réponses gzip sur iOS

Lorsqu’une application iOS reçoit du JSON compressé, la capture réseau peut n’afficher que quelques dizaines de Ko, alors que les données transmises à l’analyseur sont beaucoup plus volumineuses. Pour vérifier ce que l’application accepte réellement, inutile d’attendre qu’une API en production rencontre un problème : lancez un service de test qui n’écoute que sur la machine locale d’un Mac cloud, puis interrogez-le depuis un simulateur iOS exécuté sur ce même Mac. Nous utiliserons trois réponses — valide, avec une valeur de contrôle altérée et à fort volume après décompression — pour examiner les données renvoyées par URLSession et la limite appliquée par l’application avant l’analyse.

Distinguer la taille transférée de celle des données utilisées par l’application

gzip compresse le corps de la réponse pendant le transfert. Content-Length peut indiquer le nombre d’octets envoyés sous forme compressée, tandis que le Data reçu par le code applicatif peut déjà contenir le corps décompressé. Limiter uniquement la taille transférée n’empêche pas des caractères répétés ou des fragments JSON très semblables d’occuper beaucoup de mémoire après décompression. Pour cet exemple, la limite après décompression est fixée à 256 KiB. Il s’agit d’un seuil de test, pas d’une valeur universelle pour les API iOS : définissez le vôtre selon la taille réelle de vos réponses.

Le test doit aussi distinguer « la requête réseau a réussi » de « le corps est exploitable ». Un code d’état correct et l’absence de délai dépassé ne remplacent pas la validation du contenu. En particulier, si le flux compressé est endommagé, ne présumez pas que le système renverra toujours un type d’erreur donné. Le comportement à vérifier est que l’application ne traite jamais des données endommagées comme un résultat métier complet.

Les assertions doivent porter sur les données utilisées par l’application, et non uniquement sur les en-têtes de transfert ou sur la taille compressée affichée dans un outil réseau.

Générer trois réponses gzip en local

Enregistrez le script suivant dans /tmp/gzip_fixture.py, puis exécutez python3 /tmp/gzip_fixture.py sur le Mac cloud. Le service n’écoute que sur 127.0.0.1:8765 et n’est pas exposé à Internet. /ok renvoie un corps court ; /broken modifie la valeur de contrôle à la fin du flux gzip ; /large envoie un corps très compressible qui atteint 1 MiB après décompression. Les trois chemins transmettent de véritables données compressées et renseignent l’en-tête avec leur taille compressée.

import gzip
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer

SMALL = b'{"status":"ready"}'
LARGE = b"A" * 1_048_576

class Handler(BaseHTTPRequestHandler):
    def do_GET(self):
        if self.path not in ("/ok", "/broken", "/large"):
            self.send_error(404)
            return

        payload = LARGE if self.path == "/large" else SMALL
        wire = bytearray(gzip.compress(payload))
        if self.path == "/broken":
            wire[-8] ^= 0xFF

        self.send_response(200)
        self.send_header("Content-Type", "application/octet-stream")
        self.send_header("Content-Encoding", "gzip")
        self.send_header("Content-Length", str(len(wire)))
        self.end_headers()
        self.wfile.write(wire)

ThreadingHTTPServer(("127.0.0.1", 8765), Handler).serve_forever()

La valeur de contrôle est modifiée délibérément, plutôt que d’interrompre arbitrairement la connexion TCP : cela permet de distinguer un transfert complet dont le contenu est endommagé d’une connexion interrompue. Le script fournit des entrées fixes ; il ne prétend pas reproduire le taux de compression ni le débit d’une véritable API. Avant de lancer les tests, utilisez lsof -nP -iTCP:8765 -sTCP:LISTEN pour vérifier que le processus à l’écoute est bien celui que vous venez de démarrer. Si le port est occupé, arrêtez d’abord l’ancien service pour éviter que les tests ne contactent un autre programme.

Vérifier le corps réellement reçu dans le simulateur

Ajoutez cet exemple XCTest à une cible de test existante du projet. Il s’applique uniquement à un simulateur exécuté sur le même Mac que le service : ici, 127.0.0.1 désigne la machine locale. Avec un appareil physique, il faut repenser l’adresse du service et le contrôle d’accès. Le projet de test doit autoriser les requêtes HTTP locales dans cet environnement de développement ; n’assouplissez pas la configuration de sécurité des requêtes distantes de la version de production pour faire fonctionner ces tests.

import Foundation
import XCTest

final class GzipResponseTests: XCTestCase {
    private let base = URL(string: "http://127.0.0.1:8765")!

    private func fetch(_ path: String) async throws -> Data {
        let url = base.appendingPathComponent(path)
        let (data, response) = try await URLSession.shared.data(from: url)
        XCTAssertEqual((response as? HTTPURLResponse)?.statusCode, 200)
        return data
    }

    func testValidBody() async throws {
        let data = try await fetch("ok")
        XCTAssertEqual(data, Data(#"{"status":"ready"}"#.utf8))
    }

    func testLargeDecodedBodyExceedsAppLimit() async throws {
        let data = try await fetch("large")
        XCTAssertGreaterThan(data.count, 256 * 1024)
    }

    func testDamagedBodyIsNotAcceptedAsComplete() async {
        do {
            let data = try await fetch("broken")
            XCTAssertNotEqual(
                data,
                Data(#"{"status":"ready"}"#.utf8)
            )
        } catch {
            return
        }
    }
}

Exécutez d’abord le test de la réponse valide, puis ceux du grand corps et du corps endommagé. Si le premier reçoit les octets gzip bruts plutôt que le texte attendu, vérifiez les en-têtes de réponse, l’environnement de test et l’implémentation de la requête. Ne remplacez pas simplement l’assertion par « toute donnée reçue suffit ». Le test du corps endommagé accepte deux résultats observables : la requête échoue ou les données renvoyées diffèrent du corps complet attendu. Il ne garantit pas que le système rejettera une valeur de contrôle incorrecte à une étape précise, et ne remplace pas la validation des champs JSON par la logique métier.

Appliquer la limite de taille avant l’analyse

Dans le code de l’application, vérifiez Data.count avant de transmettre les données au décodeur. L’exemple ci-dessous applique un seuil après réception de la réponse complète : il évite de poursuivre l’analyse d’un corps trop volumineux, mais n’empêche pas la consommation de mémoire déjà survenue pendant le téléchargement et la décompression. Si l’API peut renvoyer un contenu extrêmement volumineux, prévoyez aussi une réception par blocs, un stockage temporaire sur disque ou une limite côté serveur. Cette seule vérification ne suffit pas.

enum ResponseError: Error {
    case tooLarge
}

func checkedBody(_ data: Data) throws -> Data {
    guard data.count <= 256 * 1024 else {
        throw ResponseError.tooLarge
    }
    return data
}

Faites ensuite évoluer le test du grand corps pour vérifier que checkedBody(data) lève tooLarge, tandis que le corps normal doit être accepté. Le test ne se contente alors plus de constater une différence : il confirme que l’application applique effectivement la limite. Si votre API renvoie du JSON, décodez-le après le contrôle de taille et vérifiez les champs obligatoires. Ne confondez pas « analysable » avec « provenant de l’API attendue ».

Conserver une liste de vérifications reproductibles

Avant de soumettre ces tests, vérifiez cinq points : le service n’écoute que sur la machine locale ; les tests utilisent explicitement un simulateur sur ce même Mac ; la réponse valide est comparée au corps complet attendu ; la réponse endommagée n’est pas acceptée comme contenu complet ; la limite de taille porte sur le Data que URLSession remet à l’application. Enfin, arrêtez le service temporaire, vérifiez que le port 8765 n’est plus à l’écoute et assurez-vous que la cible de test n’a pas introduit d’exception pour le HTTP local dans la version de production.

Ce jeu de tests reste volontairement réduit. Pour couvrir ensuite un corps vide, un code d’état erroné ou d’autres encodages, ajoutez des chemins et des assertions distincts, plutôt que de réunir plusieurs défaillances dans une réponse dont le comportement varie. Des entrées fixes, une limite explicite et la vérification des octets réellement utilisés par l’application offrent une base bien plus sûre qu’un jugement fondé uniquement sur la taille compressée visible dans une capture réseau.

Questions fréquentes

Content-Length suffit-il pour limiter la taille ?

Non. Cet en-tête mesure les octets compressés transmis. La limite applicative doit porter sur le Data reçu après décompression.

Une somme de contrôle gzip erronée doit-elle toujours provoquer une exception ?

Le test ne doit pas imposer une forme précise d’échec. L’application doit refuser de traiter la réponse altérée comme un contenu complet, par erreur ou par échec de validation.

Envie de tester par vous-même ?

Louez un Mac mini dédié et lancez vos builds

Choisissez parmi deux configurations M4 dédiées, puis sélectionnez la durée de location et la région adaptées à vos besoins. La disponibilité est actualisée dans la console.

Louer un Mac mini