Gzip tronqué : le fichier de guide qui s’est téléchargé avec succès et qui était corrompu quand même

Un membre gzip coupé en plein téléchargement se décompresse en un document court, valide, plausible. Rien ne lève d’erreur, et si votre pipeline filtre sur un hash, plus rien ne la lèvera jamais.

var truncated = Gzip(Payload)[..^8];
await using var input = new MemoryStream(truncated);
await using var inflated = new MemoryStream();
await using (var gz = new GZipStream(input, CompressionMode.Decompress, leaveOpen: true))
{
    await gz.CopyToAsync(inflated);
}

Assert.Equal(Encoding.UTF8.GetByteCount(Payload), inflated.Length);

Huit octets retirés à la fin d’un fichier gzip, et le décompresseur rend la charge utile entière sans rien lever. Cette assertion se trouve dans notre suite de tests parce que la prémisse devait être épinglée avant de construire quoi que ce soit dessus. Aucune approche fondée sur les exceptions n’aurait pu attraper cela, et c’est pourquoi la garde lit le trailer du fichier lui-même au lieu d’attendre qu’on le lui dise.

Les huit octets qui ont disparu sont le trailer, la seule partie du fichier qui aurait pu contredire ce qui en est sorti.

Corrompez ces huit octets au lieu de les retirer, et le même décompresseur lève aussitôt. J’ai inversé un seul bit à chacune des huit positions du trailer, et chacune a levé InvalidDataException sans rien livrer. Le lecteur contrôle le trailer scrupuleusement quand il peut en voir un. La troncature est dangereuse parce qu’elle enlève le trailer au lieu de l’abîmer, et un contrôle qui n’est jamais atteint équivaut à pas de contrôle.

L’ingestion décrite ci-dessous s’exécute sur des adresses de guide que nous sélectionnons, sur des fichiers que leurs éditeurs servent à tout le monde. Une adresse de guide que vous fournissez est téléchargée et lue sur votre propre appareil, et nous ne détenons aucune de ces données. Les deux décompresseurs sont le même fichier porté d’une base de code à l’autre, avec les plafonds par défaut divergés de huit pour un, donc un seul article couvre les deux.

Un membre qui finit trop tôt peut quand même finir proprement

Un fichier gzip est une suite de membres, et chaque membre est un en-tête de dix octets suivi d’extras facultatifs, un flux deflate, et un trailer de huit octets portant un CRC32 suivi de l’ISIZE, la taille décompressée modulo 2^32. Dans le flux deflate, chaque bloc porte un bit qui dit s’il est le dernier bloc du jeu de données. Le décompresseur s’arrête quand il a lu ce bit et que ce bloc s’est terminé. Rien dans cette séquence ne l’oblige à lire le trailer, et si le trailer est absent, rien ne signale qu’on en attendait un. Le flux est terminé parce que le flux a dit qu’il était terminé.

La pensée suivante, évidente, est qu’une coupe plus profonde, tombant dans les données deflate, doit au moins mettre le lecteur à court d’entrée quelque part où il s’en aperçoit. Ce n’est pas le cas, et cela m’a surpris. J’ai retiré de 1 à 50 octets à la fin d’un fichier à membre unique et le décompresseur a renvoyé une sortie partielle à chaque profondeur sans un mot : 90 octets récupérés pour une coupe d’un octet, 67 pour une coupe de vingt octets, 48 pour une coupe de quarante octets. Il n’existe aucune profondeur à laquelle le runtime commence à se plaindre.

Ce qui rend la coupe à la frontière du membre dangereuse, c’est donc la forme de la sortie, pas le volume du silence. Coupez plus profond et le lecteur XML reçoit en général un élément qui ne se ferme jamais et rejette le fichier, ce qui est exactement ce que dit notre propre commentaire sur le chemin de guide à l’échelle du foyer. Coupez à la frontière et la sortie est octet pour octet un préfixe du vrai document, bien formé jusqu’à son dernier élément complet, et les deux champs qui l’auraient contredit sont les deux qui étaient encore en transit quand la connexion a disparu. Un guide qui arrive ainsi ressemble à une source aux listes maigres, ce qui est une plainte différente d’un guide dont tous les programmes sont décalés d’une heure et appelle une réparation différente.

Les coupes sur lesquelles nous pouvons raisonner viennent de HTTP, qui délimite un corps de deux façons. Un corps plus court qu’un Content-Length déclaré est une réponse cassée et le client le dit à voix haute : face à un socket qui promettait 100 octets et en a envoyé 40, .NET a levé HttpRequestException enveloppant HttpIOException, « The response ended prematurely ». Envoyez les mêmes 40 octets sans en-tête de longueur et fermez la connexion, et le même client les renvoie sans erreur, parce qu’un corps délimité par la fermeture est complet par définition, sauf si la connexion elle-même a signalé une faute. Un proxy qui abandonne au milieu d’un corps produit exactement cela : un flux qui se termine, volontairement pour autant qu’une couche inférieure puisse le dire.

Le contrôle cherche un nombre qu’il connaît déjà

Le voici en entier. Il mérite d’être lu ligne par ligne, parce que presque chaque ligne est une concession à quelque chose de réel.

const int MinGzipLength = 18;
const int TailWindow = 512;

if (!input.CanSeek || input.Length < MinGzipLength || written is <= 0 or > uint.MaxValue)
{
    return;
}

var take = (int)Math.Min(TailWindow, input.Length);
var tail = new byte[take];
input.Position = input.Length - take;
await input.ReadExactlyAsync(tail, cancellationToken).ConfigureAwait(false);

var expected = (uint)written;
for (var i = 0; i + 8 <= tail.Length; i++)
{
    if (BinaryPrimitives.ReadUInt32LittleEndian(tail.AsSpan(i + 4, 4)) == expected)
    {
        return;
    }
}

var trailing = BinaryPrimitives.ReadUInt32LittleEndian(tail.AsSpan(take - 4, 4));
if (trailing > written)
{
    throw new InvalidDataException(
        "Gzip stream is truncated: the trailer reports more bytes than were decompressed.");
}

Le 18 est un en-tête plus un trailer sans rien entre les deux, donc c’est un plancher pour « trop court pour contenir un trailer », pas un test de validité. Rien de réel ne s’en approche : le plus petit membre que cet encodeur produit pour un seul octet d’entrée mesure 21. L’exigence de positionnement est la première vraie limite : un appelant qui lit en flux direct ne peut pas être contrôlé, et le contrôle se retire au lieu de rejeter ces appelants. Nos trois chemins d’ingestion décompressent depuis un fichier ou un tampon, donc les trois sont contrôlés.

Le retrait sur > uint.MaxValue est la seconde limite, et elle est structurelle. L’ISIZE est la taille décompressée modulo 2^32, donc une fois qu’une exécution a décompressé quatre gibioctets, le champ ne peut plus être comparé à rien. C’est la largeur du champ qui parle : le contrôle s’éteint tout seul par exécution, en silence, exactement pour les plus gros guides, et tout plafond absolu fixé au-dessus de ce point achète de la marge en dépensant le contrôle de troncature.

Puis le balayage. Il lit les 512 derniers octets, parcourt chaque décalage de cette fenêtre, et lit le second de chaque paire de mots de quatre octets comme ISIZE candidat. Une correspondance n’importe où signifie qu’un membre d’exactement la taille que nous avons décompressée se termine quelque part dans cette queue, ce qui tolère un fichier rembourré. Cela a un coût : 505 décalages candidats contre une valeur de quatre octets donnent une fausse correspondance à peu près une fois sur huit millions et demi si les octets de queue n’ont aucun rapport avec nous. C’est de l’arithmétique, pas de la mesure, et les fenêtres se chevauchent, donc ce ne sont pas des tirages indépendants.

Si aucun décalage ne correspond, les quatre derniers octets sont lus comme un nombre et comparés. Un trailer qui annonce moins d’octets que nous en avons écrits est cohérent avec le dernier de plusieurs membres concaténés, donc il passe. Un trailer qui en annonce plus ne peut pas venir d’un fichier complet, donc il échoue. Cette asymétrie est tout le verdict, et c’est là que l’honnêteté doit se loger. Quand le trailer a disparu, ces quatre octets sont des données compressées lues comme une longueur, et le contrôle ne fonctionne que parce qu’une valeur 32 bits arbitraire est en général plus grande que le nombre d’octets que nous avons décompressés. À 40 MB décompressés, cela passe à peu près une fois sur cent sept. À deux gigaoctets décompressés, c’est 0.47, et parce que le nombre dans cette fraction est ce que nous avons réellement décompressé et non la vraie taille du guide, les chances sont les pires pour la coupe qui compte le plus : celle qui arrive presque complète.

La garde s’affaiblit donc à mesure que les guides grossissent, ce qui est le mauvais sens, et les plus gros guides sont ceux dont les téléchargements durent assez longtemps pour être coupés.

Deux des trois tâches sont déjà faites par le runtime

La boucle qui itère sur les membres concaténés porte dans son gestionnaire d’exception ce que j’ai longtemps cru être la logique de remplissage.

catch (Exception ex) when (ex is InvalidDataException or EndOfStreamException)
{
    // Trailing padding after a member that ended cleanly is tolerated;
    // some upstreams append junk after the gzip trailer. Anything else
    // is a truncated download, and swallowing it would write a partial
    // guide, hash it as the content, and then skip every later fetch as
    // "unchanged" until the upstream bytes happen to move.
    if (completedMembers == 0 || guard.Written > writtenBefore)
    {
        throw;
    }

    break;
}

J’ai instrumenté une réplique fidèle de cette boucle sur douze formes d’entrée, à la fois en flux mémoire et en flux fichier, et la clause catch s’est déclenchée zéro fois sur les 24 exécutions. GZipStream concatène les membres lui-même, donc deux membres arrivent en une seule lecture de 180 octets et la boucle compte un membre unique. Il tolère lui-même les octets parasites de fin, donc un fichier rembourré ne lève jamais non plus. Sur chaque forme que notre suite de tests épingle, la boucle fait exactement une itération et le contrôle du trailer prend les trois décisions : accepter le fichier rembourré, rejeter le fichier tronqué, rejeter le fichier dont le second membre a été coupé. Le gestionnaire est un vestige, et son commentaire est le seul endroit où la règle de remplissage est écrite.

Cela compte parce que la vraie règle est plus étroite que le commentaire. La tolérance au remplissage vient du balayage de 512 octets, donc elle dure exactement tant que le vrai trailer reste dans la fenêtre. J’ai balayé des longueurs de remplissage de 0 à 520 octets : 504 octets de remplissage sont acceptés et 505 sont rejetés comme tronqués. Au-delà, le contrôle retombe sur les quatre derniers octets de remplissage et les lit comme une longueur. Un remplissage de zéros survit à la frontière uniquement par accident, puisque quatre octets nuls se lisent comme une longueur de rien et que la règle « accepter plus petit » les laisse passer. Un remplissage de 0xFF à la même longueur se lit comme 4 294 967 295 et se fait rejeter.

Je n’ai jamais échantillonné le comportement que ce commentaire décrit. Des amonts qui ajoutent des octets parasites après le trailer, c’est une assertion dans notre source et un nom de test, pas une observation.

Deux plafonds vivent à côté du contrôle du trailer

La même boucle de copie applique deux limites qui n’ont rien à voir avec la troncature : un plafond absolu sur les octets écrits, et un plafond de ratio décompressé sur compressé, qui attrape un petit fichier qui se décompresse en quelque chose qui n’est pas un guide. Les deux sont testés dans la boucle, sur chaque bloc de 80 KB, si bien qu’une bombe échoue en cours de route au lieu d’après l’écriture complète.

Je ne publie pas nos chiffres actuels, et nos propres documents en sont la raison. La spécification du service, datée du 19 juillet 2026, consigne encore une paire de plafonds de récupération que la configuration livrée a depuis dépassés. Ce qui les a fait bouger, c’est un vrai guide national qui s’est avéré peser environ 200 MB compressé et se décompresser en à peu près deux gigaoctets : le plafond compressé de l’époque se trouvait exactement sur cette ligne, et le plafond décompressé rejetait le fichier d’emblée. Un nombre dans un article vieillit de la même façon qu’un nombre dans une spécification. Donc : plafonnez la taille absolue, plafonnez le ratio, testez les deux pendant l’écriture, et gardez le plafond absolu sous le point où l’ISIZE cesse d’être comparable, sinon la marge vous coûte le contrôle de troncature.

Deux détails pèsent plus que les valeurs. Le plafond de ratio ne s’applique que lorsque l’appelant connaît la taille compressée, donc un appelant qui passe zéro obtient le plafond absolu seul, et nos tests de troncature font exactement cela à dessein. Et le chemin de guide à l’échelle du foyer sur l’appareil n’appelle jamais le décompresseur partagé : il enveloppe un flux gzip brut dans un flux compteur qui applique un plafond d’octets et aucun plafond de ratio, puis appelle directement le contrôle du trailer. Celui-là est une lacune plutôt qu’une décision, et je n’ai pu le trouver écrit nulle part.

Le filtre qui rend un corps défectueux permanent

Maintenant, la moitié dont personne ne parle.

L’ingestion de guide est coûteuse et la plupart des récupérations ne changent rien, donc la récupération est conditionnelle : un If-None-Match et un If-Modified-Since tirés de ce que la dernière exécution a stocké, et un 304 termine l’exécution. Les validateurs ne suffisent pas à eux seuls. Un index public que nous résolvons sert un ETag sans guillemets, qui ne fait pas l’aller-retour, donc les requêtes conditionnelles contre lui ne court-circuitent jamais. Le filtre qui fait autorité est un SHA-256 des octets décompressés, comparé au hash stocké sur la ligne de la source.

Un hash de contenu est le meilleur filtre à tous les égards qui comptent. Il est calculé localement, et aucun amont ne peut le tromper en mentant sur ses validateurs ou en les oubliant. Il n’a pas non plus de porte de sortie, et c’est tout le problème. Le hash est pris à la récupération, sur le fichier temporaire décompressé, avant qu’un analyseur ait regardé un seul élément, donc le filtre ne peut pas distinguer un corps qui a fusionné proprement d’un corps qui a explosé deux étapes plus tard.

Trois étapes et l’échec est permanent :

  1. Un corps arrive qui ne devrait pas être accepté, coupé court ou simplement inutilisable.
  2. Son hash est calculé et stocké comme hash de contenu de la source.
  3. Chaque récupération ultérieure des mêmes octets donne le même hash et renvoie « ignoré, inchangé » sans que le corps atteigne de nouveau un analyseur.

La condition de rétablissement est que les octets amont bougent par hasard, la formule même qu’emploie le commentaire du décompresseur. Pour un guide national régénéré chaque nuit, c’est un jour. Pour un fichier que personne ne régénère, c’est jamais, et la mécanique de nouvelle tentative qui existe pour les échecs transitoires ne peut pas aider, parce qu’à partir de la seconde exécution il n’y a plus d’échec à retenter.

Le dernier tour de vis : cet état se déclare sain. Une exécution ignorée comme inchangée signifie que l’amont est joignable et que le guide est à jour, donc la tâche de santé la compte comme un succès et reporte le précédent nombre de chaînes en corroboration. Une source figée sur le hash d’un corps qui n’a jamais fusionné ressemble, sur chaque tableau de bord que nous avons, à une source qui va bien.

Nous avions l’écriture au mauvais endroit

Notre propre version de ceci n’avait rien à voir avec la troncature. C’était une question d’ordre.

Les validateurs étaient auparavant écrits immédiatement après la récupération, avant l’analyse et avant la fusion. Une exécution qui récupérait un bon corps puis échouait plus tard, sur un hoquet de base de données, sur une mise à mort pour dépassement mémoire, sur une tâche de maintenance qui à l’époque supprimait toutes les tables de staging par exécution y compris une qu’une ingestion en cours utilisait encore, enregistrait l’exécution comme échouée et stockait quand même le nouveau hash. Le cycle suivant récupérait les octets identiques, trouvait la correspondance, et sautait. La source devenait périmée et se déclarait saine, et aucune quantité de nouvelles tentatives n’y touchait.

La collision des tables de staging est corrigée pour son propre compte désormais : la maintenance saute toute table dont l’exécution est encore marquée en cours et plus jeune que le seuil d’orphelin. La correction de l’ordre est un seul appel déplacé, et le commentaire là où l’écriture atterrit désormais dit pourquoi :

// Validators persist only after the merge has succeeded. Writing
// them right after the fetch made any parse or merge failure
// permanent: the run was marked failed, but the new hash was already
// stored, so every later fetch of the same bytes short-circuited to
// skipped_unchanged and the source never re-merged until upstream
// changed. With the write here, a failed run leaves the old
// validators in place and the next cycle retries the full ingest.

Le chemin 304 conserve sa propre écriture, et celle-là est sûre, parce qu’un 304 prouve que le contenu correspond à ce qui est déjà fusionné et que rafraîchir les validateurs ne peut rien masquer. La règle est de n’écrire les validateurs qu’une fois que le corps a fini de vous être utile.

Le test de régression a la forme que je voudrais sur tout filtre de ce genre. Il monte un vrai amont qui sert 22 caractères de XML cassé, vérifie que l’exécution a échoué avec le hash de contenu stocké encore null, puis la relance sur des octets identiques et vérifie qu’elle a échoué une seconde fois au lieu de sauter. Un test miroir prouve que l’optimisation de saut fonctionne toujours après un succès, pour que la correction ne puisse pas devenir « arrêter de mettre en cache » par accident.

Nous avons trouvé l’original en lisant notre propre code lors d’un audit de plateforme en août 2026, pas dans un rapport, et c’est la partie inconfortable. L’audit a produit 110 constats et a classé celui-ci en sévérité haute. Je ne peux pas vous dire combien de fois il s’est déclenché en production, parce que l’état qu’il produit est indiscernable de la santé, et que le guide qu’il sert est vieux au lieu d’être absent.

L’appareil a la même forme et pas de hash

La moitié serveur est corrigée. La moitié appareil ne l’est pas, et je préfère l’écrire plutôt que de laisser cette page suggérer une symétrie.

L’import de guide par source de notre Application Windows stocke son ETag et son Last-Modified une ligne après le retour de la récupération, avant la décompression, avant l’analyse, avant que quoi que ce soit atteigne le magasin local. Aucun chemin de code ne les efface ensuite. Le puits d’échec supprime la génération de guide à moitié écrite et laisse les métadonnées de récupération intactes ; la libération ne touche ni l’un ni l’autre. Donc un amont qui respecte les requêtes conditionnelles peut servir un corps que nous échouons à analyser, recevoir nos validateurs rafraîchis, et répondre à la requête suivante par un 304 qui ne fusionne rien.

Deux choses l’atténuent, et aucune ne le referme. L’appareil ne conserve aucun hash de contenu, seulement les deux validateurs, donc le piège a besoin d’un amont qui respecte réellement les requêtes conditionnelles, et ceux dont les validateurs sont cassés sont immunisés par accident. Et un import échoué laisse le guide précédent lisible, parce que le pointeur de génération n’est basculé qu’à l’achèvement, donc le symptôme visible est un guide qui cesse d’avancer au lieu d’un guide qui disparaît. C’est un meilleur échec que celui du serveur, et c’est quand même le même bug.

Le chemin à l’échelle du foyer, celui qui rassemble les guides de tout un foyer dans un seul magasin, s’en sort correctement d’une façon intéressante. Il ne peut pas décompresser vers un tampon puis contrôler, parce que l’agrégat est trop gros pour tenir, donc il analyse directement depuis le flux gzip et valide le trailer ensuite. Avant de valider, il vide ce que le lecteur XML a laissé derrière lui, parce que le compte transmis au contrôle doit être le membre entier et non le point où le lecteur s’est arrêté par hasard. Analyser d’abord, vider, puis contrôler. Cet ordre est pénible à lire et c’est le seul qui fonctionne.

Ce que cela vous apporte

Notre Application Windows exécute ce décompresseur sur votre machine, contre l’adresse de guide que vous lui avez donnée, et aucun flux ne passe par nous à aucun moment. Un guide qui échoue au contrôle du trailer laisse à l’écran celui que vous avez déjà, ce qui est le comportement que vous attendez d’un rafraîchissement en arrière-plan que vous ne regardiez pas. Vous pouvez créer un compte gratuit et le pointer vers une source que vous avez déjà.

Si vous ne retenez qu’une habitude de cette page, que ce soit de vous méfier du mot « inchangé ». Un filtre de cache qui repose sur le contenu d’un corps est le bon filtre et celui que je reconstruirais, et c’est aussi un filtre qui n’a aucun moyen de dire « j’ai accepté ce corps et ensuite tout ce qui a suivi a mal tourné ». Quoi que vous calculiez à la porte, stockez-le au point où vous savez que le travail a réussi, et donnez à votre contrôle de santé quelque chose à regarder qui ne soit pas l’absence d’erreur.

Ce que cet article a mesuré32 affirmations, chacune avec les preuves derrière elles
RéclamationPreuveCompté
Un fichier gzip est une suite de membres ; chaque membre est un en-tête fixe de dix octets suivi de champs facultatifs, un flux deflate, et un trailer de huit octets contenant le CRC32 suivi de l’ISIZE, la taille décompressée modulo 2^32.RFC 1952 section 2.2 (file format, series of members), section 2.3 (member format), section 2.3.1 (member header and trailer, which defines both CRC32 and ISIZE as the size of the original input modulo 2^32). https://www.rfc-editor.org/rfc/rfc1952.htmlSpécificationSans objet
Chaque bloc deflate porte un bit BFINAL qui indique s’il est le dernier bloc du jeu de données, si bien qu’un décompresseur peut terminer un flux sans jamais lire ce qui le suit.RFC 1951 section 3.2.3: "BFINAL is set if and only if this is the last block of the data set." https://www.rfc-editor.org/rfc/rfc1951.htmlSpécificationSans objet
Une réponse sans longueur déclarée, dont le corps se termine quand la connexion se ferme, est traitée comme complète sauf si la connexion sous-jacente a signalé une erreur, si bien que le client n’a rien à quoi comparer la longueur reçue.RFC 9112 section 6.3 rule 8 (body length is the octets received before the server closes the connection) and section 8, Handling Incomplete Messages. https://www.rfc-editor.org/rfc/rfc9112.htmlSpécificationSans objet
Avec le trailer de huit octets entièrement retiré, GZipStream renvoie la charge utile complète et ne lève rien.n = 123 août 2026
Aucune profondeur de coupe n’est détectable au niveau du décompresseur : retirer de 1 à 50 octets d’un fichier à membre unique renvoie une sortie partielle et ne lève rien à chaque profondeur, 90 octets récupérés pour une coupe de 1 octet et 48 octets récupérés pour une coupe de 40 octets.n = 5023 août 2026
Un trailer présent mais corrompu lève bel et bien une erreur : un seul bit inversé à n’importe laquelle des huit positions d’octet du trailer lève InvalidDataException sans rien livrer.n = 823 août 2026
Cinq longueurs de troncature sont épinglées comme échecs du pipeline : 1, 4, 8, 20 et 40 octets retirés d’un membre construit sur un document de 90 octets, et les cinq rejets viennent du contrôle du trailer plutôt que de GZipStream.n = 523 août 2026
Le contrôle du trailer lit une fenêtre de queue de 512 octets, ignore les fichiers de moins de 18 octets et les flux non positionnables, parcourt chaque décalage à la recherche d’un trailer candidat dont le second mot égale le nombre d’octets décompressés, et seulement si aucun ne correspond lit les quatre derniers octets et échoue quand ils dépassent ce qui a été décompressé.n = 123 août 2026
Le plancher de 18 octets est un en-tête plus un trailer sans rien entre les deux ; le plus petit membre gzip réel est plus grand, à 21 octets pour un octet d’entrée.n = 123 août 2026
505 décalages candidats contre une valeur de quatre octets donnent une fausse correspondance d’environ une sur huit millions et demi quand les octets de queue n’ont aucun rapport avec le compte décompressé.Arithmetic: the loop condition i + 8 <= 512 runs i in [0, 504], so 505 offsets; 505 * 2^-32 = 1.18e-7, or 1 in 8.50 million. Offset count confirmed by measurement.SpécificationSans objet
Quand le trailer a disparu, les quatre derniers octets sont des données compressées lues comme une longueur, et le contrôle accepte dès que cette valeur n’est pas supérieure au compte décompressé : environ une fois sur cent sept pour 40 MB décompressés, et 0.47 pour deux gigaoctets décompressés.Arithmetic: P(accept) = written / 2^32; 40e6/2^32 = 0.0093 (1 in 107.4), 2e9/2^32 = 0.466.SpécificationSans objet
Le remplissage après un membre propre n’est toléré que tant que le vrai trailer reste dans la fenêtre de 512 octets : 504 octets de remplissage sont acceptés et 505 sont rejetés comme tronqués.n = 52123 août 2026
GZipStream concatène les membres et tolère les octets parasites de fin par lui-même, si bien que sur chaque forme épinglée par la suite de tests, la boucle de membres fait une itération, sa clause catch se déclenche zéro fois, et chaque verdict vient du contrôle du trailer.n = 1223 août 2026
Le contrôle se retire quand le compte décompressé dépasse uint.MaxValue, parce que l’ISIZE est la taille modulo 2^32, si bien qu’une exécution dont la sortie atteint quatre gibioctets perd le contrôle en silence.n = 123 août 2026
Les trois chemins d’ingestion décompressent depuis un fichier ou un tampon, si bien que le contrôle dépendant du positionnement s’exécute sur les trois.n = 323 août 2026
La boucle de copie applique un plafond absolu d’octets et un plafond de ratio compressé sur décompressé, tous deux testés sur chaque bloc de 80 KB ; le plafond de ratio ne s’applique que lorsque l’appelant fournit une taille compressée.n = 123 août 2026
Les deux décompresseurs sont le même fichier porté d’une base de code à l’autre, avec le plafond absolu par défaut divergé de huit pour un.n = 223 août 2026
Le chemin appareil à l’échelle du foyer enveloppe son décompresseur dans un flux compteur qui applique un plafond d’octets et aucun plafond de ratio du tout.n = 123 août 2026
La spécification du service consigne encore une paire de plafonds de récupération que la configuration livrée a déjà dépassés.n = 123 août 2026
Un vrai guide national pèse environ 200 MB compressé et se décompresse en à peu près deux gigaoctets ; le plafond compressé de l’époque se trouvait exactement sur cette ligne et le plafond décompressé rejetait le fichier d’emblée.n = 123 août 2026
L’ingestion récupère de façon conditionnelle et filtre sur un SHA-256 des octets décompressés, parce qu’au moins un amont sert un ETag qui ne fait pas l’aller-retour.n = 123 août 2026
Le hash est calculé à la récupération, avant qu’un analyseur voie les octets, si bien qu’un échec d’analyse et un échec de fusion sont indiscernables pour le filtre.n = 123 août 2026
Un corps plus court qu’un Content-Length déclaré lève HttpRequestException enveloppant HttpIOException, "The response ended prematurely" ; les mêmes 40 octets livrés en mode délimité par fermeture reviennent sans la moindre erreur.n = 223 août 2026
Les validateurs étaient auparavant persistés immédiatement après la récupération, ce qui rendait permanent tout échec d’analyse ou de fusion ; ils ne sont désormais persistés qu’après une fusion réussie, et le chemin 304 conserve sa propre écriture.n = 123 août 2026
L’audit qui l’a trouvé a produit 110 constats et a classé celui-ci en sévérité haute.n = 11011 août 2026
Une tâche de maintenance planifiée supprimait autrefois toutes les tables de staging par exécution, y compris une qu’une ingestion en cours utilisait encore, ce qui était le producteur concret d’un échec en cours d’exécution ; elle exclut désormais les tables dont l’exécution est encore en cours et plus jeune que le seuil d’orphelin.n = 123 août 2026
Une exécution ignorée comme inchangée compte comme un succès de santé et reporte le précédent nombre de chaînes vérifiées.n = 123 août 2026
Le test de régression sert 22 caractères de XML cassé, vérifie que l’exécution échoue avec le hash de contenu encore null, et vérifie qu’une seconde exécution sur des octets identiques échoue de nouveau au lieu d’être ignorée ; un test miroir prouve que l’optimisation de saut fonctionne toujours après un succès.n = 223 août 2026
Le chemin appareil par source stocke son ETag et son Last-Modified une ligne après le retour de la récupération, avant la décompression et l’analyse, et aucun chemin de code ne les efface, si bien qu’un échec d’analyse peut être suivi d’un 304 qui ne fusionne rien.n = 123 août 2026
Le chemin appareil par source ne conserve qu’un ETag et un Last-Modified, sans hash de contenu, si bien que sa version du piège dépend du respect des requêtes conditionnelles par l’amont.n = 123 août 2026
Un import échoué laisse le guide précédent lisible, parce que le pointeur de génération n’est basculé qu’à l’achèvement.n = 123 août 2026
Le chemin à l’échelle du foyer analyse d’abord et valide le trailer ensuite, en vidant ce que le lecteur XML a laissé pour que le compte transmis au contrôle soit le membre entier.n = 123 août 2026