mod_h2: HTTP/2 stream 1 was not closed cleanly: PROTOCOL_ERROR

Hi there We are seeing the following problem:

  • When trying to do a HEAD on a PHP-File we get n “PROTOCOL_ERROR(0x01) / HTTP/2 stream 1 was not closed cleanly” error message.
  • GET works fine.
  • Only happens with HEAD to PHP-Files. HEAD to HTML Files for example works fine.
  • Happens with curl but also with nghttp-Client…
  • Does not happen with nginx
  • Apache v2.4.34 / libnghttp2 v1.32.0 / curl v7.61.0 / FreeBSD 10.4

Thanks for taking a look at it.

curl HEAD output:

$ curl -4 --http2 -I -v https://danii.ch/phpinfo.php
*   Trying x.x.x.x...
* TCP_NODELAY set
* Connected to danii.ch (x.x.x.x) port 443 (#0)
* ALPN, offering h2
* ALPN, offering http/1.1
* successfully set certificate verify locations:
*   CAfile: /etc/ssl/certs/ca-certificates.crt
  CApath: none
* TLSv1.2 (OUT), TLS handshake, Client hello (1):
* TLSv1.2 (IN), TLS handshake, Server hello (2):
* TLSv1.2 (IN), TLS handshake, Certificate (11):
* TLSv1.2 (IN), TLS handshake, Server key exchange (12):
* TLSv1.2 (IN), TLS handshake, Server finished (14):
* TLSv1.2 (OUT), TLS handshake, Client key exchange (16):
* TLSv1.2 (OUT), TLS change cipher, Client hello (1):
* TLSv1.2 (OUT), TLS handshake, Finished (20):
* TLSv1.2 (IN), TLS handshake, Finished (20):
* SSL connection using TLSv1.2 / ECDHE-RSA-AES256-GCM-SHA384
* ALPN, server accepted to use h2
* Server certificate:
*  subject: CN=www.danii.ch
*  start date: May  5 00:00:00 2018 GMT
*  expire date: Nov 20 12:00:00 2018 GMT
*  subjectAltName: host "danii.ch" matched cert's "danii.ch"
*  issuer: C=US; O=DigiCert Inc; OU=www.digicert.com; CN=Encryption Everywhere DV TLS CA - G2
*  SSL certificate verify ok.
* Using HTTP2, server supports multi-use
* Connection state changed (HTTP/2 confirmed)
* Copying HTTP/2 data in stream buffer to connection buffer after upgrade: len=0
* Using Stream ID: 1 (easy handle 0x561a794c7f90)
> HEAD /phpinfo.php HTTP/2
> Host: danii.ch
> User-Agent: curl/7.61.0
> Accept: */*
> 
* Connection state changed (MAX_CONCURRENT_STREAMS == 100)!
* HTTP/2 stream 1 was not closed cleanly: PROTOCOL_ERROR (err 1)
* Connection #0 to host danii.ch left intact
curl: (92) HTTP/2 stream 1 was not closed cleanly: PROTOCOL_ERROR (err 1)

curl GET output:

*   Trying x.x.x.x...
* TCP_NODELAY set
* Connected to danii.ch (x.x.x.x) port 443 (#0)
* ALPN, offering h2
* ALPN, offering http/1.1
* successfully set certificate verify locations:
*   CAfile: /etc/ssl/certs/ca-certificates.crt
  CApath: none
* TLSv1.2 (OUT), TLS handshake, Client hello (1):
* TLSv1.2 (IN), TLS handshake, Server hello (2):
* TLSv1.2 (IN), TLS handshake, Certificate (11):
* TLSv1.2 (IN), TLS handshake, Server key exchange (12):
* TLSv1.2 (IN), TLS handshake, Server finished (14):
* TLSv1.2 (OUT), TLS handshake, Client key exchange (16):
* TLSv1.2 (OUT), TLS change cipher, Client hello (1):
* TLSv1.2 (OUT), TLS handshake, Finished (20):
* TLSv1.2 (IN), TLS handshake, Finished (20):
* SSL connection using TLSv1.2 / ECDHE-RSA-AES256-GCM-SHA384
* ALPN, server accepted to use h2
* Server certificate:
*  subject: CN=www.danii.ch
*  start date: May  5 00:00:00 2018 GMT
*  expire date: Nov 20 12:00:00 2018 GMT
*  subjectAltName: host "danii.ch" matched cert's "danii.ch"
*  issuer: C=US; O=DigiCert Inc; OU=www.digicert.com; CN=Encryption Everywhere DV TLS CA - G2
*  SSL certificate verify ok.
* Using HTTP2, server supports multi-use
* Connection state changed (HTTP/2 confirmed)
* Copying HTTP/2 data in stream buffer to connection buffer after upgrade: len=0
* Using Stream ID: 1 (easy handle 0x5613edf1df90)
> GET /phpinfo.php HTTP/2
> Host: danii.ch
> User-Agent: curl/7.61.0
> Accept: */*
> 
* Connection state changed (MAX_CONCURRENT_STREAMS == 100)!
< HTTP/2 200 
HTTP/2 200 
< date: Mon, 06 Aug 2018 13:39:32 GMT
date: Mon, 06 Aug 2018 13:39:32 GMT
< server: Apache/2.4
server: Apache/2.4
< content-type: text/html; charset=UTF-8
content-type: text/html; charset=UTF-8

< 
* Connection #0 to host danii.ch left intact

HEAD via Google Chrome Console:

var http = new XMLHttpRequest();
http.open("HEAD", "https://prognolite.com/header.php");
http.send(); console.log(http);

Failed to load resource: net::ERR_SPDY_PROTOCOL_ERROR

About this issue

  • Original URL
  • State: closed
  • Created 6 years ago
  • Comments: 37 (17 by maintainers)

Commits related to this issue

Most upvoted comments

Thanks a lot @icing and @mkauf! A backport to 2.4.x would be awesome.

Proposed in r1854966.

Correct, thanks for the reminder. I have a bunch of other changes which are to be merged, but I should do this one independently.

@mkauf, finding all my mistakes… 😃

I’ll make a version tomorrow with this change and, if verified by the folks here, bring it into the Apache subversion space.

The bug occurs if the client performs a HEAD request and if the content generator module calls ap_rflush() before generating body data. mod_h2 then inserts the headers and tries to remove body buckets (there are none). This code has a bug, and it removes the headers that it just inserted.

This bugfix works for me:

diff --git a/modules/http2/h2_from_h1.c b/modules/http2/h2_from_h1.c
index d69c53c21b..eaf0c93e28 100644
--- a/modules/http2/h2_from_h1.c
+++ b/modules/http2/h2_from_h1.c
@@ -594,18 +594,20 @@ apr_status_t h2_filter_headers_out(ap_filter_t *f, apr_bucket_brigade *bb)
         }
     }
     
-    if (r->header_only) {
+    if (r->header_only || AP_STATUS_IS_HEADER_ONLY(r)) {
         ap_log_cerror(APLOG_MARK, APLOG_TRACE1, 0, f->c,
-                      "h2_task(%s): header_only, cleanup output brigade", 
+                      "h2_task(%s): headers only, cleanup output brigade", 
                       task->id);
         b = body_bucket? body_bucket : APR_BRIGADE_FIRST(bb);
         while (b != APR_BRIGADE_SENTINEL(bb)) {
             next = APR_BUCKET_NEXT(b);
             if (APR_BUCKET_IS_EOS(b) || AP_BUCKET_IS_EOR(b)) {
                 break;
-            } 
-            APR_BUCKET_REMOVE(b);
-            apr_bucket_destroy(b);
+            }
+            if (!H2_BUCKET_IS_HEADERS(b)) {
+                APR_BUCKET_REMOVE(b);
+                apr_bucket_destroy(b);
+            }
             b = next;
         }
     }

I added AP_STATUS_IS_HEADER_ONLY() to remove body buckets if the HTTP status code implies that the response has no body (204 No Content / 304 Not Modified). I think that’s necessary because Apache does it for HTTP 1.x in ap_http_header_filter().