# ERR_HTTP2_PROTOCOL_ERROR: Find the Broken Response

> Debug Chrome ERR_HTTP2_PROTOCOL_ERROR and curl (92): compare HTTP versions, inspect invalid headers and body lengths, then trace the failing hop.

Source: https://howhttpworks.com/debug/err-http2-protocol-error
Last reviewed: 2026-10-05

Error messages this page covers:
- `net::ERR_HTTP2_PROTOCOL_ERROR`
- `ERR_HTTP2_PROTOCOL_ERROR`
- `curl: (92) HTTP/2 stream 1 was not closed cleanly: PROTOCOL_ERROR (err 1)`

> **TL;DR:** Something on the path broke HTTP/2 rules and the stream was reset. The usual culprits are HTTP/1.1 headers like `Connection` leaking into HTTP/2, an invalid header name or value, or a `Content-Length` that doesn't match the body (often after gzip). Reproduce the exact URL with `curl -v --http2` and `curl -v --http1.1`; if only HTTP/2 fails, read the headers and stream reset with `nghttp -nv` or a Chrome NetLog, then check proxy disk space and any TLS inspection on the path.

## What it means

Chrome's `net::ERR_HTTP2_PROTOCOL_ERROR` is a network error, not an HTTP status. You may see `200` next to it in the console: the headers arrived fine and the failure came later, in the body. Capture the failing request rather than guessing from the page.

curl reports the same failure like this (the stream number will vary):

```text
curl: (92) HTTP/2 stream 1 was not closed cleanly: PROTOCOL_ERROR (err 1)
```

That wording comes from [curl's 8.10.1 source](https://github.com/curl/curl/blob/curl-8_10_1/lib/http2.c); other releases phrase it differently. The two numbers come from different places: [`92` is curl's error code](https://curl.se/libcurl/c/libcurl-errors.html) for an HTTP/2 stream failure, and `err 1` is HTTP/2's PROTOCOL_ERROR code. Neither is an HTTP status.

## Confirm which protocol fails

Test the exact failing path, query string included. The home page loading fine tells you nothing about a broken download or login callback.

```bash
curl --version
curl -v --http2 -o /dev/null 'https://example.com/failing-path'
curl -v --http1.1 -o /dev/null 'https://example.com/failing-path'
nghttp -nv 'https://example.com/failing-path'
```

Make sure `curl --version` lists HTTP2, and check the verbose output to confirm the first request really negotiated `h2`, since [`--http2`](https://curl.se/docs/manpage.html#--http2) quietly falls back to HTTP/1.1. Send the same cookies, auth and headers as the browser. Avoid `curl -I`: a HEAD response has no body, and the body is often where the problem is.

If both versions fail, you're looking at an ordinary origin or proxy error; start there. If HTTP/1.1 works and HTTP/2 fails, you know the problem depends on the protocol, but you still don't know which hop causes it. If you can reach the origin directly, repeat the test there:

```bash
# Replace the documentation IP with the actual origin IP.
curl -v --http2 --resolve example.com:443:203.0.113.10 \
  -o /dev/null 'https://example.com/failing-path'
```

`--resolve` sends the request to that IP while keeping the real hostname for Host, SNI and certificate verification. An HTTP/1.1-only origin can't reproduce the edge's HTTP/2 failure, but check its headers and body anyway; the bad header or length often starts there.

## Fix the response producer

### HTTP/1.1 connection headers leaked into HTTP/2

HTTP/2 forbids `connection`, `keep-alive`, `proxy-connection`, `transfer-encoding` and `upgrade`. A gateway translating from HTTP/1.x has to strip them, along with any header that `Connection` lists. The one exception is `te: trailers`, which is allowed on requests. See [RFC 9113 §8.2.2](https://www.rfc-editor.org/rfc/rfc9113#section-8.2.2).

Look at middleware that copies upstream headers wholesale, and at any custom response-header rules in your proxy. Fix the translation layer itself, and leave these headers alone on the HTTP/1.1 leg, where they still mean something. A common offender is an app that adds `Connection: keep-alive` as an "optimization"; it has no place on an HTTP/2 response.

### Invalid header names or values

In HTTP/2, header names must be lowercase. Values can't contain CR, LF or NUL, and can't start or end with a space or tab ([§8.2.1](https://www.rfc-editor.org/rfc/rfc9113#section-8.2.1)). An empty name fails HTTP's field-name grammar ([RFC 9110 §5.1](https://www.rfc-editor.org/rfc/rfc9110#section-5.1)); an empty value is fine.

The bad header is usually one built at runtime: copied user input, debug metadata, or a redirect `Location`. Watch out for a false lead here. Uppercase names are legal on an HTTP/1.1 origin, and gateways normally lowercase them, so seeing `Content-Type` at the origin doesn't mean uppercase bytes reached the HTTP/2 leg. Check what the gateway actually encoded, or the detail in the client's rejection.

### Content-Length disagrees with the body

When a response has content, `content-length` must equal the number of bytes sent in DATA frames. HEAD and 304 responses are the exception: they carry the length of a body they don't send, so an empty body there isn't a mismatch ([RFC 9113 §8.1.1](https://www.rfc-editor.org/rfc/rfc9113#section-8.1.1)).

Compression and response rewriting are the usual causes. [The length counts the encoded bytes](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Content-Length), so a length computed before gzip is wrong once gzip runs. Have whichever layer produces the final body set the length, or leave it out when the final size isn't known yet. To check, download the raw bytes; leave off `--compressed`, because it decodes them:

```bash
curl -sS --http2 -H 'Accept-Encoding: gzip' \
  -D response.headers -o response.body 'https://example.com/failing-path'
wc -c < response.body
```

Compare that number with `content-length` in `response.headers`. Note curl's exit status and error too, since a failed transfer leaves a partial file that will look short.

## Check proxy storage and TLS inspection

**A buffered response can fail after its headers have gone out.** When an upstream response doesn't fit in memory, nginx writes the overflow to disk under [`proxy_temp_path`](https://nginx.org/en/docs/http/ngx_http_proxy_module.html#proxy_temp_path). If that write fails, the [event pipe](https://github.com/nginx/nginx/blob/master/src/event/ngx_event_pipe.c) aborts and the [upstream handler](https://github.com/nginx/nginx/blob/master/src/http/ngx_http_upstream.c) terminates the request mid-transfer. The HTTP/2 error code the client reports varies, so treat this as a cause to check, not a match on the error text.

Find the failed request in nginx's error log. If you see `No space left on device` for a temporary file, check the filesystem that holds the temp path, which may not be the one with your document root:

```bash
nginx -T 2>&1 | grep -E 'proxy_temp_path|proxy_buffering'
# Substitute the actual path from configuration or the error log.
df -h /var/lib/nginx/proxy
df -i /var/lib/nginx/proxy
```

Free up space (or inodes) and replay the request. Turning `proxy_buffering off` changes how every response streams; fix the disk instead of reaching for it.

**Try a path without HTTPS inspection.** A device that decrypts TLS sits in the middle of the HTTP/2 exchange, so its bugs surface as protocol errors. [Cisco's release notes](https://www.cisco.com/c/en/us/td/docs/security/secure-firewall/release-notes/threat-defense/760/threat-defense-release-notes-76.html) list CSCwi34730, a TLS-decryption defect that produced this browser error. Endpoint antivirus can inspect HTTPS too; [ESET documents its SSL/TLS filtering](https://support.eset.com/en/kb3126-disable-ssl-filtering-in-eset-windows-products), for example.

Load the same URL from another machine on another network. On the affected machine, look at the certificate issuer: an enterprise or security-product CA means something is inspecting traffic. If the failure follows the inspected path, ask its administrator for a scoped bypass test and check the appliance or endpoint logs. Remove the bypass once you've found the faulty component.

## Capture the rejection in Chrome

1. Open `chrome://net-export` and click **Start Logging To Disk**.
2. Keep that tab open. Reproduce the failure in another tab, then click **Stop Logging**.
3. Load the JSON in the [NetLog viewer](https://netlog-viewer.appspot.com/). [Chromium's capture guide](https://www.chromium.org/for-testers/providing-network-details/) links the viewer's source for local use.
4. Find the failing URL and its HTTP/2 session. Search for `HTTP2_SESSION_RECV_INVALID_HEADER`, `HTTP2_STREAM_ERROR`, `RST_STREAM` and `GOAWAY`, and note the direction, stream ID and error detail for each.

NetLogs can include cookies and other sensitive request data, especially if you capture raw bytes, so keep them private. When Chrome receives a reset, that tells you which HTTP/2 peer sent it: the hop directly in front of the browser. The bad response may have started further upstream.

[`nghttp -nv`](https://nghttp2.org/documentation/nghttp.1.html) prints every frame and header and throws away the response body. Save its output alongside timestamped server logs. One capture beats clearing the browser cache over and over.

## Cloudflare-specific checks

[Cloudflare's HTTP/2 guide](https://developers.cloudflare.com/speed/optimization/protocol/http2/) names three causes: malformed origin headers, gzip applied without updating `Content-Length`, and broken gzip content. It recommends inspecting the origin directly and reviewing origin compression. One diagnostic it documents is turning off compression at the origin and letting Cloudflare compress instead.

Request the exact URL through Cloudflare and directly at the origin, and match both against the origin logs. Change one compression layer at a time and rerun both protocol tests. If the failure only shows up through Cloudflare, this comparison tells you whether the origin or the edge is responsible.

## Related

- [HTTP/1.1 vs HTTP/2](https://howhttpworks.com/compare/http1-vs-http2): what changes between the two protocol tests.
- [Connection](https://howhttpworks.com/headers/connection): headers that a gateway must remove during translation.
- [Content-Length](https://howhttpworks.com/headers/content-length): the body size the receiver is promised.
- [ERR_CONNECTION_RESET](https://howhttpworks.com/debug/err-connection-reset): investigate a TCP reset when the transport itself is torn down.
