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
- 400 Bad Request
- 401 Unauthorized
- 402 Payment Required
- 403 Forbidden
- 404 Not Found
- 405 Method Not Allowed
- 406 Not Acceptable
- 407 Proxy Authentication Required
- 408 Request Timeout
- 409 Conflict
- 410 Gone
- 411 Length Required
- 412 Precondition Failed
- 413 Content Too Large
- 414 URI Too Long
- 415 Unsupported Media Type
- 416 Range Not Satisfiable
- 417 Expectation Failed
- 418 (Unused)
- 419 Page Expired
- 422 Unprocessable Content
- 423 Locked
- 424 Failed Dependency
- 425 Too Early
- 426 Upgrade Required
- 428 Precondition Required
- 429 Too Many Requests
- 431 Request Header Fields Too Large
- 444 No Response
- 451 Unavailable For Legal Reasons
- 494 Request Header Too Large
- 495 SSL Certificate Error
- 496 SSL Certificate Required
- 497 HTTP Request Sent to HTTPS Port
- 499 Client Closed Request