Skip to content
Control Plane Labs

421 Misdirected Request

Client error response defined by RFC 9110 §15.5.20.

Last updated July 27, 2026

Common causes at a glance

  • HTTP/2 coalescing across hostnames covered by one wildcard or multi-SAN certificate
  • A shared TLS terminator whose vhost map is narrower than the certificate it presents
  • A request whose :authority or Host header does not match any configured server name
  • A stale connection reused after DNS changed to point the hostname at different infrastructure

Reproduce it

curl -i --http2 --resolve app.example.com:443:203.0.113.10 https://app.example.com/

What 421 Misdirected Request means

HTTP/2 clients reuse connections aggressively. If two hostnames resolve to the same address and the presented certificate is valid for both, a browser may send requests for the second host over the connection already open for the first, with no new handshake. This is coalescing, and it is a real performance win.

It breaks when the certificate is broader than the server configuration. The terminator proves authority for b.example.com, but the vhost behind it only knows a.example.com, so a coalesced request for the second name lands somewhere that cannot answer it. 421 says exactly that, and invites a retry on a fresh connection.

What the spec says

RFC 9110 §15.5.20 says an origin server sends 421 to reject a target URI that does not match a configured origin (§4.3.1) or does not match the connection context it arrived over (§7.4). Two clauses matter operationally. A client may retry a 421 on a different connection even if the method is not idempotent, a deliberate exception to the usual retry rules, because nothing was processed. And a proxy must never generate 421. The coalescing behaviour behind the condition is RFC 9113 §9.1.1, which names 421 as the way a server signals it does not want connections reused across origins.

What actually causes it

The signature case is a wildcard certificate in front of several independently configured backends. A browser opens a connection for www.example.com, notices that api.example.com resolves to the same IP and is covered by the same *.example.com certificate, and reuses the connection. But the load balancer routes purely on SNI, negotiated for the first name only. The result is intermittent, depending on request order and on whether the connection was still open. Multi-tenant platforms produce the same effect when tenants share a certificate but not a routing table.

How to debug it

The tell is that 421 is intermittent under HTTP/2 and disappears under HTTP/1.1, where coalescing does not exist. Test both:

curl -i --http1.1 https://api.example.com/ | head -1
curl -i --http2 https://api.example.com/ | head -1

If only the HTTP/2 request fails, look at the certificate. Dump the SAN list with openssl s_client and compare every name it covers against the hostnames your terminator serves. Any name in the certificate that is missing from the vhost map is a candidate for a misdirected request.

Two fixes work: narrow the certificates so unrelated origins never share one, or widen the server configuration so every name in the certificate is routable, which at a shared edge means routing on the :authority pseudo-header rather than SNI alone. The header inspector shows when two names that should be distinct are answered by the same backend.

Working examples

curl

# Coalescing only exists in HTTP/2+. If 1.1 works and 2 does not, that is it.
curl -i --http1.1 https://api.example.com/ | head -1
curl -i --http2  https://api.example.com/ | head -1

# Which names does the certificate claim authority for?
openssl s_client -connect api.example.com:443 -servername api.example.com </dev/null 2>/dev/null \
  | openssl x509 -noout -ext subjectAltName

# Pin both hostnames to one IP over one connection to reproduce deliberately.
curl -i --http2 \
  --resolve api.example.com:443:203.0.113.10 \
  --resolve www.example.com:443:203.0.113.10 \
  https://www.example.com/ https://api.example.com/

Python (requests)

import requests

# urllib3 does not coalesce across hostnames, so requests rarely sees 421.
# It still shows up behind a shared edge with a mismatched vhost map.
for host in ("www.example.com", "api.example.com"):
    resp = requests.get(f"https://{host}/", timeout=10)
    print(host, resp.status_code)
    if resp.status_code == 421:
        # Safe to retry even for POST: the server processed nothing.
        retry = requests.get(f"https://{host}/", timeout=10)
        print("retry on a fresh connection:", retry.status_code)

Node (fetch)

// undici keeps an HTTP/2 pool per origin, so reproduce by hitting both names.
for (const host of ["www.example.com", "api.example.com"]) {
  const res = await fetch(`https://${host}/`);
  console.log(host, res.status);

  if (res.status === 421) {
    // RFC 9110 permits retrying a 421 on a new connection for any method.
    const retry = await fetch(`https://${host}/`, { cache: "no-store" });
    console.log("retry:", retry.status);
  }
}

Reference and tooling

The authoritative list of assigned status codes is theIANA HTTP Status Code Registry. For a searchable copy with the semantics summarised, use theHTTP status code reference on this site. To see exactly what a server is sending, theheader inspector shows the raw status line and response headers, and thecurl builder assembles the request flags for you.

Other 4xx codes