In short. ERR_HTTP2_PROTOCOL_ERROR means Chrome received something over HTTP/2 that violates the protocol and had to abort the connection. It is a server or proxy fault in the overwhelming majority of cases. The single fastest test is to repeat the request over HTTP/1.1: if that succeeds while HTTP/2 fails, no amount of clearing cache, updating the browser or disabling extensions will help.
Read the error literally
Chrome raises this when its HTTP/2 implementation receives a frame it cannot accept and terminates the stream or the whole connection. The protocol has no tolerance mode: unlike HTTP/1.1, where a slightly malformed response is often parsed anyway, HTTP/2 is a binary framing protocol and a violation is fatal by design.
That single fact rules out most of the advice you will find for this error. A corrupted cache entry cannot produce an invalid HTTP/2 frame. An out-of-date browser cannot either — it either speaks the protocol or negotiates HTTP/1.1 instead. What can produce one is a server, a reverse proxy, a CDN or a middlebox emitting something the specification forbids.
There is a narrow class of genuine client-side causes: an extension or local security product that intercepts TLS and rewrites headers can corrupt the stream. That is worth ruling out — but it is the exception, and the two-command test below separates it from the common case in seconds.

The two commands that decide everything
Before changing a single setting, establish whether the failure is protocol-specific. These take about ten seconds together.
# 1. Force HTTP/2 and watch the negotiation
curl -sSv --http2 https://example.com/ -o /dev/null 2>&1 \
| grep -Ei 'alpn|using http|http/2|error|GOAWAY|RST_STREAM'
# 2. Force HTTP/1.1 and compare
curl -sS --http1.1 -o /dev/null -w 'http1.1 status: %{http_code}\n' https://example.com/
Three outcomes, and each sends you somewhere different:
| HTTP/2 | HTTP/1.1 | What it means | Where to look |
|---|---|---|---|
| fails | succeeds | Protocol-level fault on the server side | Server, proxy or CDN HTTP/2 handling |
| fails | fails | Not an HTTP/2 problem at all | The underlying error — TLS, origin, routing |
| succeeds | succeeds | The server is fine from here | The user's network, proxy or extension |
The third row is where the client-side advice finally applies — and note that you only reach it after proving the server is healthy from an independent vantage point. Running the check from outside your own network matters, because a corporate TLS-inspection appliance sits between you and the site and is itself a common cause.
Cause 1: headers that HTTP/2 forbids
HTTP/2 tightened the rules on header fields, and code written for HTTP/1.1 can emit fields that were legal then and are illegal now. Two rules break real deployments constantly.
Connection-specific fields are prohibited. RFC 9113 §8.2.2 forbids Connection, Keep-Alive, Proxy-Connection, Transfer-Encoding and Upgrade in an HTTP/2 message. An endpoint that receives one must treat it as malformed. Application code or a legacy proxy that still adds Connection: keep-alive produces exactly this error.
Field names must be lowercase. RFC 9113 §8.2.1 requires field names to be lowercase; Content-Type as a literal in an HTTP/2 frame is a violation where it was perfectly normal in HTTP/1.1.
# What is the origin actually emitting? Look at raw HTTP/1.1 headers first —
# the offending field is usually visible there before the proxy converts it.
curl -sS -D - -o /dev/null --http1.1 https://example.com/ \
| grep -iE '^(connection|keep-alive|proxy-connection|transfer-encoding|upgrade):'
# Anything printed above must not survive into an HTTP/2 response.
In practice this most often comes from an application framework setting the header itself, or from a reverse proxy configured years ago and carried forward. Removing the field at the edge is the fix:
# nginx — stop passing a connection-specific header upstream to downstream
proxy_hide_header Connection;
proxy_hide_header Keep-Alive;
proxy_hide_header Transfer-Encoding;
Cause 2: headers that are simply too large
HTTP/2 peers advertise SETTINGS_MAX_HEADER_LIST_SIZE, and a response whose header block exceeds what the client will accept is refused. Because HTTP/2 compresses headers with HPACK, the limit applies to the uncompressed size, so a large cookie jar counts fully against it.
The tell is that the error is not universal: it appears for logged-in users and not for anonymous ones, or it starts after a release that added a header. That pattern almost always means size.
# Measure the total header bytes the origin returns
curl -sS -D /tmp/h.txt -o /dev/null --http1.1 https://example.com/ && wc -c < /tmp/h.txt
# Which single header is the largest?
awk '{ print length($0), $0 }' /tmp/h.txt | sort -rn | head -5 | cut -c1-120
Raise the server's limits only after you know which header is responsible. The durable fix is usually to shrink it — a session cookie that grew to several kilobytes is a bug, not a capacity problem.
# nginx — HTTP/2 header table and buffer sizing
large_client_header_buffers 4 16k;
# Apache with mod_http2
H2MaxHeaderListSize 65536

Cause 3: something between you and the server rewrites the stream
Corporate TLS inspection, some endpoint security products and a few consumer antivirus suites terminate TLS locally, inspect the plaintext, and re-encrypt. Doing that correctly for HTTP/2 is considerably harder than for HTTP/1.1, and an appliance that gets ALPN or framing wrong produces this error for every user behind it while the site is perfectly healthy for everyone else.
The signature is unmistakable once you look for it: the failure follows the network, not the site. Same laptop on mobile tethering works; on the office network it fails. Different sites fail on the same network.
# Which protocol was negotiated, and who issued the certificate you received?
openssl s_client -connect example.com:443 -servername example.com -alpn h2 \
< /dev/null 2>/dev/null | grep -E 'ALPN|issuer='
# An issuer you do not recognise — a corporate CA — means TLS is being intercepted.
If the issuer is your employer's internal CA, stop debugging the website. The connection you are testing never reaches it in one piece. Report the site to whoever runs the appliance instead of changing anything on the server.
Cause 4: the server's own HTTP/2 implementation
HTTP/2 support has had genuine bugs, particularly in older nginx builds and in Apache's mod_http2 under specific configurations. Symptoms cluster around large responses, streamed responses and connection reuse under load rather than around a specific URL.
Two checks separate an implementation bug from a content problem. First, does it reproduce on a trivial static file as well as on the failing page? Second, does it correlate with concurrency — fine for one request, failing when the page issues thirty in parallel?
# Same connection, many parallel requests — reuse and concurrency stress
curl -sS --http2 -o /dev/null -w '%{http_code} ' \
https://example.com/a.css https://example.com/b.js https://example.com/c.png \
https://example.com/d.png https://example.com/e.png; echo
# Version of the server's HTTP/2 stack
nginx -V 2>&1 | tr ' ' '\n' | grep -i http_v2
apachectl -M 2>/dev/null | grep -i http2
If a static asset fails under parallelism, upgrade the server before investigating your application. If only one dynamic endpoint fails, the problem is what that endpoint emits — go back to causes 1 and 2.
Cause 5: a proxy or CDN in front, speaking two protocols at once
A typical stack negotiates HTTP/2 with the browser and HTTP/1.1 with the origin. The proxy therefore translates, and translation is where forbidden headers get carried across and where framing errors are introduced.
Determine who answered before assuming it is your server:
curl -sSI --http2 https://example.com/ \
| grep -iE '^(cf-ray|x-cache|via|server|x-served-by):'
If a proxy header is present, test the origin directly, bypassing it, and compare. If the origin is clean over both protocols and the proxied request fails over HTTP/2, the fault is in the edge layer. The HTTP header checker shows the full set from outside your network, and the protocol test reports which of HTTP/1.1, HTTP/2 and HTTP/3 your host actually negotiates.
What the common advice gets wrong, and the small part it gets right
| Common advice | Verdict | Why |
|---|---|---|
| Clear cache and cookies | Mostly wrong, occasionally useful | Cache cannot create an invalid frame — but an oversized cookie jar can push headers past the limit, which is cause 2 |
| Update the browser | Wrong | An old browser negotiates a protocol it supports; it does not invent violations |
| Disable extensions | Sometimes right | Only extensions that intercept or rewrite traffic; test in a clean profile rather than guessing |
| Disable antivirus / firewall | Right for the wrong reason | The mechanism is TLS interception, not blocking — which is cause 3 |
| Turn off HTTP/2 on the server | Last resort | Restores service by giving up multiplexing and header compression; fixes nothing and costs performance |
| Use a VPN | Diagnostic only | If it helps, you have proven a middlebox on the original path — now fix that |
Notice that the two entries with real substance — cookie size and TLS interception — are both server-side or network-side findings dressed up as browser tips. That is why the ordering matters: run the protocol comparison first, and the correct branch selects itself.

A diagnosis order that does not waste a maintenance window
- Compare protocols with the two curl commands. This alone eliminates two thirds of the possibilities.
- Reproduce from a second network. Mobile tethering is enough. Failure on one network only means a middlebox.
- Check who answered — proxy headers tell you whether your origin was even involved.
- Inspect the response headers for forbidden fields and for total size.
- Test a static asset to separate an implementation bug from application output.
- Only then consider client-side factors, and only for the users who are still affected.
Each step either fails and names the cause or passes and removes a whole class. Disabling HTTP/2 belongs at the end and only as a temporary measure, because it converts a diagnosable fault into a permanent performance cost.
How to check your site right now
Use the protocol test to see whether your host negotiates HTTP/2 and HTTP/3 correctly and what it falls back to, the header checker to read the exact response headers from outside your network — including the forbidden fields and their total size — and the SSL checker to confirm ALPN and the certificate chain, since a broken handshake can surface as a protocol error rather than a certificate warning.
This error is frequently intermittent: it follows a particular endpoint, a particular header size or a particular edge node, so a single manual check can easily land in a healthy window. Scheduled monitoring records the status code and response time of every check, which turns "some users sometimes" into a timestamped pattern you can align with releases and traffic peaks. For the neighbouring QUIC-layer error, see the ERR_QUIC_PROTOCOL_ERROR guide; for the protocol trade-offs themselves, HTTP/2 versus HTTP/3.

Frequently asked questions
Is ERR_HTTP2_PROTOCOL_ERROR a Chrome bug?
Very rarely. Chrome is reporting that it received a frame the protocol does not permit. Other HTTP/2 clients usually fail on the same response with a differently worded error, which is a good confirmation step — try curl --http2 and see whether it also refuses.
Why does the site work in one browser and not another?
Browsers differ in how strictly they enforce certain limits and in whether they fall back to HTTP/1.1 after a failure. A site that works in one and fails in another is still emitting something non-conformant; the tolerant browser is hiding it rather than proving it correct.
Should I just disable HTTP/2?
It restores service and it costs you multiplexing, header compression and a measurable amount of page speed, especially on high-latency connections. Treat it as a way to stop the bleeding while you find the real cause, not as the resolution.
Why does it only happen for logged-in users?
That pattern points at header size. Authenticated requests carry session cookies and often extra headers, so they cross a limit that anonymous requests do not. Measure the total header bytes on an authenticated response before changing any server setting.
Can a CDN cause this even when my origin is correct?
Yes. The edge speaks HTTP/2 to the browser and frequently HTTP/1.1 to your origin, so it performs a translation your origin never sees. Test the origin directly with curl --resolve and compare; if the origin is clean over both protocols, the edge is where to look.
The error appears only on our office network. Is the site broken?
No — that is the signature of TLS interception on that network. Check the certificate issuer you receive: if it is an internal corporate CA rather than a public one, the connection is being terminated and re-encrypted before it reaches you, and the appliance doing it is mishandling HTTP/2.
Checklist
- Compare HTTP/2 against HTTP/1.1 before changing anything.
- Reproduce from a second network to rule a middlebox in or out.
- Check response headers for
Connection,Keep-Alive,Transfer-EncodingandUpgrade. - Confirm field names are lowercase in the HTTP/2 path.
- Measure total header bytes, especially on authenticated responses.
- Verify the certificate issuer is public, not an internal CA.
- Test a static asset under parallelism to expose implementation bugs.
- Identify who answered before blaming your origin.
- Treat disabling HTTP/2 as triage, never as the fix.
- Record checks over time — this error is usually intermittent.