Résoudre l’erreur HTTP/2 stream was not closed cleanly

L’erreur HTTP/2 stream was not closed cleanly apparaît souvent avec curl, Git, une API, un navigateur, un CDN ou un reverse proxy. Elle indique qu’un flux HTTP/2 a été interrompu ou fermé d’une manière que le client considère comme incorrecte.

Le message exact varie selon le contexte :

curl: (92) HTTP/2 stream 0 was not closed cleanly: PROTOCOL_ERROR (err 1)Langage du code : HTTP (http)
curl: (92) HTTP/2 stream 1 was not closed cleanly: INTERNAL_ERROR (err 2)Langage du code : HTTP (http)
error: RPC failed; curl 92 HTTP/2 stream 0 was not closed cleanlyLangage du code : HTTP (http)

Le réflexe classique consiste à accuser curl, Git ou le navigateur. Pourtant, dans beaucoup de cas, le problème vient plutôt du serveur, du CDN, d’un proxy intermédiaire, d’un en-tête HTTP invalide, d’un timeout, ou d’un backend qui coupe la réponse trop tôt.

Voici une méthode propre pour diagnostiquer et corriger l’erreur, sans désactiver HTTP/2 au hasard comme on débranche une multiprise en pleine prod.

Distingo, le livret à 2%

Ce que signifie cette erreur HTTP/2

HTTP/2 fonctionne avec des streams, ou flux. Plusieurs requêtes peuvent passer sur une même connexion TCP/TLS. C’est l’un des grands intérêts du protocole : au lieu d’ouvrir une connexion par ressource, le client peut multiplexeur plusieurs échanges sur une seule connexion.

Lorsque curl indique qu’un stream n’a pas été fermé proprement, cela signifie que le client a reçu une fin de flux, une erreur ou une fermeture de connexion qui ne respecte pas ce qu’il attendait du protocole HTTP/2.

Les causes les plus fréquentes sont :

  • un serveur HTTP/2 mal configuré ;
  • un CDN ou reverse proxy qui interrompt le flux ;
  • un backend qui ferme la connexion avant d’envoyer toute la réponse ;
  • un fichier ou une réponse tronquée ;
  • des en-têtes HTTP incompatibles avec HTTP/2 ;
  • un timeout entre proxy et application ;
  • un bug ou comportement limite dans une bibliothèque HTTP/2 ;
  • une connexion réseau instable ;
  • une requête Git trop volumineuse ou interrompue ;
  • un mélange malheureux entre HTTP/2, TLS, cache et compression.

La bonne nouvelle : on peut isoler la couche fautive avec quelques tests simples.

Commencer par reproduire avec curl

Le premier test consiste à forcer HTTP/2 avec curl et à afficher les détails de la connexion.

curl -vvv --http2 -I https://www.example.com/Langage du code : JavaScript (javascript)

Dans la sortie, regardez notamment :

  • la négociation ALPN ;
  • le protocole accepté par le serveur ;
  • le certificat TLS ;
  • les en-têtes de réponse ;
  • le code HTTP ;
  • le moment exact où le stream échoue.

Vous devriez voir une ligne indiquant que le serveur accepte HTTP/2 :

ALPN: server accepted h2Langage du code : HTTP (http)

Ou, selon la version de curl :

using HTTP/2

Ensuite, comparez immédiatement avec HTTP/1.1.

curl -vvv --http1.1 -I https://www.example.com/Langage du code : JavaScript (javascript)

Si HTTP/1.1 fonctionne mais HTTP/2 échoue, le problème est bien lié à la couche HTTP/2 ou à un intermédiaire qui gère mal HTTP/2.

Kinsta: Premium Managed WordPress hosting

Tester le contenu complet, pas seulement les en-têtes

Une requête HEAD peut réussir alors qu’un téléchargement complet échoue. Testez donc aussi le corps de la réponse.

curl -vvv --http2 -o /dev/null https://www.example.com/Langage du code : JavaScript (javascript)

Pour tester un fichier précis, par exemple un PDF, une archive ou une grosse réponse JSON :

curl -vvv --http2 -o /tmp/test-download.bin https://www.example.com/fichier.zipLangage du code : JavaScript (javascript)

Si le fichier est tronqué ou si l’erreur apparaît en fin de téléchargement, suspectez un problème de taille de réponse, de buffering, de compression, de CDN, de timeout ou de fermeture prématurée côté backend.

Comparer avec et sans CDN

Si le site passe par Cloudflare, Fastly, CloudFront, Bunny, un WAF ou un reverse proxy externe, il faut distinguer le comportement du CDN et celui du serveur d’origine.

Testez d’abord l’URL publique :

curl -vvv --http2 -I https://www.example.com/Langage du code : JavaScript (javascript)

Ensuite, testez directement l’origine en forçant la résolution DNS avec --resolve. Remplacez 203.0.113.10 par l’IP réelle du serveur d’origine.

curl -vvv --http2 \
  --resolve www.example.com:443:203.0.113.10 \
  -I https://www.example.com/Langage du code : JavaScript (javascript)

Si l’origine fonctionne mais pas l’URL derrière le CDN, le problème se situe probablement dans la configuration CDN, cache, WAF, compression, HTTP/2 ou TLS côté intermédiaire.

Si l’origine échoue aussi, le problème vient plutôt de Nginx, Apache, OpenLiteSpeed, PHP-FPM, Node, Python, Ruby, du framework applicatif ou d’un en-tête généré par l’application.

Distingo, le livret à 2%

Cas Cloudflare : vérifier Browser Cache TTL

Dans mon cas initial, l’erreur venait de Cloudflare. Le réglage en cause était :

Caching > Configuration > Browser Cache TTL > Respect Existing Headers

La correction consistait à choisir une valeur explicite au lieu de Respect Existing Headers. Par exemple :

Browser Cache TTL: 4 hours

Ce n’est pas une solution universelle, mais c’est un test rapide si votre erreur apparaît uniquement derrière Cloudflare.

Vérifiez également les en-têtes de cache renvoyés par l’origine :

curl -I https://www.example.com/Langage du code : JavaScript (javascript)

Regardez notamment :

  • cache-control
  • expires
  • etag
  • last-modified
  • cf-cache-status
  • server

Si Cloudflare reçoit des consignes contradictoires, des réponses dynamiques mal déclarées, ou des en-têtes de cache incohérents, le comportement peut devenir difficile à lire. Le cache, c’est merveilleux quand il obéit. Sinon, c’est un stagiaire sous amphétamines.

Chercher des en-têtes interdits en HTTP/2

HTTP/2 n’accepte pas certains en-têtes liés à la connexion. Une réponse qui contient ces en-têtes peut être considérée comme mal formée.

Surveillez particulièrement :

  • connection
  • proxy-connection
  • keep-alive
  • transfer-encoding
  • upgrade

Inspectez les en-têtes avec HTTP/2 :

curl -vvv --http2 -I https://www.example.com/ 2>&1 | lessLangage du code : JavaScript (javascript)

Comparez ensuite avec HTTP/1.1 :

curl -vvv --http1.1 -I https://www.example.com/ 2>&1 | lessLangage du code : JavaScript (javascript)

Si l’application, un plugin, un framework ou un reverse proxy ajoute un en-tête comme Connection: keep-alive dans une réponse HTTP/2, corrigez la source de cet en-tête. Il ne faut pas simplement masquer le symptôme côté client.

Kinsta: Premium Managed WordPress hosting

Vérifier Nginx et HTTP/2

Sur Nginx récent, HTTP/2 s’active avec une directive dédiée :

server {
	listen 443 ssl;
	http2 on;

	server_name www.example.com;

	# ...
}Langage du code : PHP (php)

L’ancienne syntaxe existe encore dans beaucoup de configurations :

listen 443 ssl http2;

Mais avec les versions modernes de Nginx, il vaut mieux utiliser http2 on; dans le bloc server ou http.

Vérifiez la configuration :

sudo nginx -t

Puis rechargez Nginx :

sudo systemctl reload nginx

Vérifiez aussi les logs pendant la reproduction de l’erreur :

sudo tail -f /var/log/nginx/error.logLangage du code : JavaScript (javascript)

Si vous voyez des messages du type upstream prematurely closed connection, connection reset by peer, upstream timed out ou client prematurely closed connection, le problème peut venir du backend ou d’un timeout proxy, pas directement de HTTP/2.

Réglages Nginx utiles derrière un backend

Si Nginx sert de reverse proxy vers PHP-FPM, Node, Python, Ruby, Go ou une autre application, inspectez les timeouts.

Pour un proxy HTTP classique :

location / {
	proxy_pass http://backend;

	proxy_connect_timeout 30s;
	proxy_send_timeout 120s;
	proxy_read_timeout 120s;

	proxy_buffering on;
}Langage du code : JavaScript (javascript)

Pour une réponse en streaming, des logs en temps réel, Server-Sent Events ou certaines API longues, il faut parfois désactiver le buffering :

location /stream/ {
	proxy_pass http://backend;

	proxy_http_version 1.1;
	proxy_buffering off;
	proxy_read_timeout 3600s;
	proxy_send_timeout 3600s;
}Langage du code : JavaScript (javascript)

Pour WebSocket, ajoutez aussi les en-têtes d’upgrade côté HTTP/1.1 entre Nginx et le backend :

location /ws/ {
	proxy_pass http://backend;

	proxy_http_version 1.1;
	proxy_set_header Upgrade $http_upgrade;
	proxy_set_header Connection "upgrade";

	proxy_read_timeout 3600s;
	proxy_send_timeout 3600s;
}Langage du code : PHP (php)

Attention : ces en-têtes concernent la connexion entre Nginx et le backend. Ne les injectez pas aveuglément dans une réponse HTTP/2 envoyée au navigateur.

Vérifier Apache et mod_http2

Sur Apache, HTTP/2 passe par mod_http2. Dans un VirtualHost TLS, on utilise généralement :

<VirtualHost *:443>
	ServerName www.example.com

	Protocols h2 http/1.1

	SSLEngine on
	SSLCertificateFile /etc/letsencrypt/live/www.example.com/fullchain.pem
	SSLCertificateKeyFile /etc/letsencrypt/live/www.example.com/privkey.pem

	# ...
</VirtualHost>Langage du code : HTML, XML (xml)

Vérifiez que le module est chargé :

apachectl -M | grep http2

Sur Debian ou Ubuntu :

sudo a2enmod http2
sudo systemctl reload apache2

Apache documente aussi l’impact de HTTP/2 sur les ressources serveur : des workers HTTP/2 supplémentaires peuvent être utilisés, et HTTP/2 garde plus d’état par connexion que HTTP/1.1. Sur un site chargé, il faut donc surveiller mémoire, workers et limites de streams.

Tester avec nghttp

curl suffit dans beaucoup de cas. Pour un diagnostic HTTP/2 plus précis, utilisez aussi nghttp, fourni par le paquet nghttp2-client.

Sur Debian ou Ubuntu :

sudo apt update
sudo apt install nghttp2-client

Testez ensuite l’URL :

nghttp -nv https://www.example.com/Langage du code : JavaScript (javascript)

L’outil affiche les frames HTTP/2, les headers, les settings, les streams et les erreurs éventuelles. C’est plus verbeux, mais très utile quand curl vous donne seulement un message générique.

Cas Git : error RPC failed; curl 92

Git peut aussi afficher cette erreur lors d’un clone, pull, fetch ou push, surtout sur un dépôt volumineux ou une connexion instable.

Exemple :

error: RPC failed; curl 92 HTTP/2 stream 0 was not closed cleanly
fatal: the remote end hung up unexpectedlyLangage du code : HTTP (http)

Commencez par tester le dépôt avec HTTP/1.1 :

git -c http.version=HTTP/1.1 clone https://github.com/user/repository.gitLangage du code : PHP (php)

Si cela fonctionne, vous pouvez configurer Git temporairement ou globalement pour utiliser HTTP/1.1 :

git config --global http.version HTTP/1.1Langage du code : PHP (php)

Pour annuler ce réglage :

git config --global --unset http.versionLangage du code : PHP (php)

Évitez de commencer par augmenter http.postBuffer au hasard. Ce vieux réflexe traîne partout, mais il ne corrige pas forcément un problème HTTP/2. Testez d’abord le protocole, le réseau, le proxy, le VPN et le serveur Git.

Cas WordPress : erreurs cURL, API et webhooks

Dans WordPress ou WooCommerce, cette erreur peut apparaître lors d’un appel externe : licence de plugin, API REST, webhook Stripe, service de livraison, connexion à une plateforme SaaS, cron, update checker, ou requête HTTP côté serveur.

Testez d’abord depuis le serveur :

curl -vvv --http2 https://api.example.com/Langage du code : JavaScript (javascript)

Puis en HTTP/1.1 :

curl -vvv --http1.1 https://api.example.com/Langage du code : JavaScript (javascript)

Si HTTP/1.1 fonctionne et HTTP/2 échoue, vous pouvez forcer HTTP/1.1 dans votre appel WordPress uniquement pour l’endpoint problématique.

Exemple propre dans un plugin ou un mu-plugin :

<?php
/**
 * Force HTTP/1.1 for a specific remote API when HTTP/2 fails upstream.
 *
 * @package SkyMinds
 */

add_action(
	'http_api_curl',
	static function ( $handle, array $parsed_args, string $url ): void {
		$host = wp_parse_url( $url, PHP_URL_HOST );

		if ( 'api.example.com' !== $host ) {
			return;
		}

		curl_setopt( $handle, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_1 );
	},
	10,
	3
);Langage du code : HTML, XML (xml)

Ce contournement doit rester ciblé. Ne forcez pas tout WordPress en HTTP/1.1 sans diagnostic, sinon vous cachez peut-être un vrai problème serveur.

Vérifier les logs PHP-FPM et backend

Si le serveur ferme la réponse avant la fin, le problème peut venir de PHP-FPM, d’une limite mémoire, d’un timeout, d’une erreur fatale ou d’un processus tué.

Sur un serveur PHP-FPM :

sudo journalctl -u php8.3-fpm --since "1 hour ago"Langage du code : JavaScript (javascript)

Selon la distribution :

sudo tail -f /var/log/php8.3-fpm.log
sudo tail -f /var/log/php-fpm/error.logLangage du code : JavaScript (javascript)

Pour Nginx :

sudo tail -f /var/log/nginx/error.log
sudo tail -f /var/log/nginx/access.logLangage du code : JavaScript (javascript)

Pour Apache :

sudo tail -f /var/log/apache2/error.log
sudo tail -f /var/log/apache2/access.logLangage du code : JavaScript (javascript)

Cherchez surtout :

  • fatal error PHP ;
  • memory exhausted ;
  • upstream timed out ;
  • prematurely closed connection ;
  • connection reset by peer ;
  • worker killed ;
  • gateway timeout ;
  • réponse tronquée ;
  • erreur TLS ;
  • limite de taille de buffer.

Désactiver HTTP/2 : solution ou pansement ?

Désactiver HTTP/2 peut être un contournement utile pour confirmer le diagnostic. Mais ce n’est pas toujours la bonne solution finale.

Pour tester côté client :

curl --http1.1 https://www.example.com/Langage du code : JavaScript (javascript)

Pour Git :

git -c http.version=HTTP/1.1 fetch

Pour Nginx, désactiver HTTP/2 temporairement revient à retirer ou couper la directive :

server {
	listen 443 ssl;
	http2 off;

	server_name www.example.com;
}

Ou, avec une ancienne configuration :

listen 443 ssl;

Si le problème disparaît, vous avez confirmé la zone de recherche. Ensuite, cherchez la vraie cause : headers, backend, CDN, TLS, buffering, timeout ou bug applicatif.

Vérifier TLS et ALPN

HTTP/2 sur le web moderne passe généralement par TLS avec négociation ALPN. Vérifiez ce que le serveur annonce.

openssl s_client -alpn h2 -connect www.example.com:443 -servername www.example.com < /dev/nullLangage du code : JavaScript (javascript)

Dans la sortie, cherchez :

ALPN protocol: h2

Si ALPN ne négocie pas h2, le serveur ne propose pas HTTP/2 sur cette connexion, ou le test atteint un autre endpoint que prévu.

Réponses compressées, gros fichiers et téléchargements interrompus

Certains problèmes apparaissent uniquement sur les grosses réponses : gros JSON, export CSV, archive ZIP, PDF, sauvegarde, flux vidéo ou endpoint API volumineux.

Testez avec et sans compression :

curl -vvv --http2 -H "Accept-Encoding: identity" -o /tmp/output.bin https://www.example.com/exportLangage du code : JavaScript (javascript)

Puis avec compression :

curl -vvv --http2 --compressed -o /tmp/output.bin https://www.example.com/exportLangage du code : JavaScript (javascript)

Si l’erreur apparaît seulement avec compression ou seulement sur les grosses réponses, regardez les buffers Nginx, la compression gzip/Brotli, le CDN, le backend et les timeouts.

Tester une API lente ou un traitement long

Une API qui met longtemps à répondre peut déclencher des fermetures de flux si un proxy intermédiaire estime que le backend ne répond plus.

Mesurez les temps avec curl :

curl \
  --http2 \
  -o /dev/null \
  -s \
  -w "dns=%{time_namelookup}s connect=%{time_connect}s tls=%{time_appconnect}s first_byte=%{time_starttransfer}s total=%{time_total}s\n" \
  https://www.example.com/api/slow-endpointLangage du code : JavaScript (javascript)

Si time_starttransfer est très élevé, le serveur met longtemps à envoyer le premier octet. Il faut alors regarder le backend : requête SQL lente, appel API externe, worker saturé, file d’attente, verrou, cache froid ou tâche trop lourde.

Cas WordPress derrière Cloudflare

Sur un site WordPress derrière Cloudflare, cette erreur peut venir d’un mélange entre cache, headers, plugin d’optimisation, compression, redirections et serveur d’origine.

Vérifiez dans cet ordre :

  1. testez l’URL avec curl --http2 ;
  2. testez la même URL avec curl --http1.1 ;
  3. testez directement l’origine avec --resolve ;
  4. désactivez temporairement les règles Cloudflare suspectes ;
  5. vérifiez Browser Cache TTL ;
  6. purgez le cache Cloudflare ;
  7. désactivez temporairement Brotli, Rocket Loader ou règles de transformation si elles sont actives ;
  8. désactivez le plugin de cache WordPress seulement pour tester ;
  9. regardez les logs Nginx/Apache/PHP-FPM ;
  10. corrigez les en-têtes invalides ou la cause backend.

Ne changez pas dix réglages d’un coup. Sinon, vous aurez peut-être corrigé le problème, mais vous ne saurez pas pourquoi. Et dans deux mois, le bug reviendra avec un petit sourire.

Checklist de diagnostic rapide

  • Reproduire avec curl -vvv --http2.
  • Comparer avec curl -vvv --http1.1.
  • Tester les en-têtes avec -I.
  • Tester le corps complet avec -o /dev/null.
  • Comparer CDN et origine avec --resolve.
  • Inspecter les en-têtes interdits en HTTP/2.
  • Vérifier Nginx ou Apache.
  • Vérifier les logs PHP-FPM ou backend.
  • Tester les gros fichiers avec et sans compression.
  • Vérifier Cloudflare, cache, Browser Cache TTL et règles CDN.
  • Tester Git en HTTP/1.1 si l’erreur apparaît lors d’un clone ou fetch.
  • Ne désactiver HTTP/2 globalement qu’en dernier recours, ou temporairement pour confirmer.

Solutions selon la cause

Cause probableTest utileCorrection
Problème CloudflareComparer CDN et origine avec --resolveAjuster cache, Browser Cache TTL, règles, compression
En-tête HTTP/2 invalideComparer les headers HTTP/2 et HTTP/1.1Supprimer Connection, Transfer-Encoding, Upgrade, etc.
Backend trop lentMesurer time_starttransferOptimiser backend, SQL, cache, timeouts
Réponse tronquéeTélécharger le fichier complet avec curlVérifier buffers, compression, CDN, logs backend
Git échoue en HTTP/2git -c http.version=HTTP/1.1Forcer HTTP/1.1 pour Git, vérifier réseau et serveur
Nginx mal configurénginx -t et logsCorriger http2 on;, timeouts, proxy, buffers
Apache mod_http2 saturéLogs Apache et ressourcesAjuster workers, streams, mémoire, MPM
API externe instableTester depuis le serveur avec HTTP/1.1 et HTTP/2Contournement ciblé en HTTP/1.1, retry, timeout

FAQ

Que signifie curl: (92) HTTP/2 stream was not closed cleanly ?

Cette erreur indique qu’un flux HTTP/2 a été interrompu ou fermé d’une manière considérée comme incorrecte par le client. La cause vient souvent du serveur, d’un proxy, d’un CDN, d’un backend ou d’en-têtes incompatibles avec HTTP/2.

Pourquoi HTTP/1.1 fonctionne alors que HTTP/2 échoue ?

HTTP/2 est plus strict sur le format des messages et gère les requêtes en streams multiplexés. Une configuration acceptable en HTTP/1.1 peut donc échouer en HTTP/2, notamment à cause d’en-têtes invalides, d’un proxy ou d’un backend mal géré.

Faut-il désactiver HTTP/2 pour corriger l’erreur ?

Pas en premier. Désactiver HTTP/2 peut confirmer le diagnostic ou servir de contournement temporaire, mais il vaut mieux corriger la cause : headers, CDN, timeouts, backend, compression ou configuration serveur.

Cloudflare peut-il provoquer cette erreur ?

Oui. Un réglage de cache, une règle Cloudflare, une réponse d’origine incohérente, la compression ou une interaction avec HTTP/2 peut provoquer l’erreur. Testez toujours avec et sans Cloudflare lorsque c’est possible.

Comment corriger l’erreur avec Git ?

Testez d’abord la commande Git en HTTP/1.1 avec git -c http.version=HTTP/1.1 fetch ou clone. Si cela fonctionne, configurez Git en HTTP/1.1 temporairement ou globalement, puis vérifiez aussi la stabilité réseau, le proxy, le VPN et le serveur Git.

Quels en-têtes peuvent casser HTTP/2 ?

Les en-têtes liés à la connexion comme Connection, Proxy-Connection, Keep-Alive, Transfer-Encoding et Upgrade ne doivent pas être envoyés dans un message HTTP/2.

Conclusion

L’erreur HTTP/2 stream was not closed cleanly n’a pas une cause unique. Elle peut venir de Cloudflare, Nginx, Apache, Git, cURL, d’un proxy, d’un backend, d’un timeout ou d’en-têtes incompatibles avec HTTP/2.

La méthode fiable consiste à comparer HTTP/2 et HTTP/1.1, tester CDN et origine, inspecter les headers, télécharger le contenu complet, lire les logs serveur, puis corriger la couche responsable.

HTTP/2 apporte de vraies améliorations, mais il pardonne moins les configurations bancales. Et c’est plutôt une bonne chose : il force le ménage. Même si, parfois, il le fait avec la délicatesse d’un huissier à 7 h 02.

Besoin d’aide pour corriger une erreur HTTP/2, cURL ou Cloudflare ?

Si votre site WordPress, votre boutique WooCommerce ou votre serveur renvoie des erreurs HTTP/2, cURL, Cloudflare, Nginx, Apache ou PHP-FPM, je peux vous aider à isoler la vraie couche responsable.

J’interviens comme développeur WordPress, WooCommerce et spécialiste serveur pour analyser les logs, les en-têtes HTTP, la configuration TLS, les règles CDN, les timeouts backend, les appels API et les problèmes de performance réseau.

  • Diagnostic HTTP/2, HTTP/1.1, TLS, ALPN, CDN et reverse proxy.
  • Analyse des erreurs cURL, Git, API, webhooks WooCommerce et appels externes.
  • Correction des en-têtes invalides, timeouts, buffers Nginx/Apache et règles Cloudflare.
  • Audit PHP-FPM, WordPress, plugins de cache, optimisations serveur et logs applicatifs.
  • Intervention documentée, testée et réversible, sans désactiver HTTP/2 au hasard.

Vous voulez comprendre pourquoi le flux HTTP/2 casse au lieu de multiplier les réglages au hasard ? Contactez-moi. Je vous aiderai à corriger le vrai problème, pas seulement l’erreur qui s’affiche dans le terminal.

À lire aussi sur SkyMinds

Sources

Demandez à l'IA son opinion
Gravatar for Matt Biscay

Je suis Matt Biscay, développeur WordPress & WooCommerce certifié chez Codeable, administrateur système et enseignant.

J’aide les entreprises à créer, optimiser et fiabiliser leurs sites WordPress avec une approche technique propre : performance, sécurité, maintenance, développement sur mesure et résolution de problèmes complexes.

Sur Skyminds, je partage des tutoriels WordPress, WooCommerce, Linux et administration système, avec des solutions testées sur des cas réels et pensées pour durer.

Découvrez mes services WordPress et WooCommerce.

Opinions