HTTP 425 is Too Early: the server is unwilling to risk processing a request that might be replayed (RFC 8470 section 5.2). It exists for TLS 1.3 early data, also called 0-RTT. When a client resumes an earlier session, it can send its first request before the handshake finishes and save a round trip, but anyone who recorded that early data can send it again, and the server can’t always tell the copy from the original.
How the pieces fit
- The client that sends a request in early data must be ready for a 425. RFC 8470 says a user agent should retry automatically, and never in early data again; Firefox does.
- A TLS-terminating proxy that forwards a request before its own handshake completes must add
Early-Data: 1, so the origin knows the request is replayable. - The origin must answer 425 to a request marked
Early-Data: 1that it can’t safely process. It shouldn’t send 425 otherwise, since a client that didn’t use early data may not know what to do with it. - Proxies in between must pass a 425 back when the request they forwarded carried
Early-Data.
POST /api/transfers HTTP/1.1
Host: bank.example
Early-Data: 1
Content-Type: application/json
HTTP/1.1 425 Too Early
The client sends the request again once the handshake is done, and the proxy forwards it without Early-Data.
Which requests to let through
Replays hurt only requests with side effects. A common rule is to accept GET, HEAD and OPTIONS in early data and answer 425 to everything else. That only holds if your GETs really are safe: a GET /unsubscribe?id=... that changes state shouldn’t run from early data either.
nginx
nginx’s documentation pairs ssl_early_data with forwarding the $ssl_early_data variable, which is 1 while the handshake is incomplete:
server {
listen 443 ssl;
ssl_protocols TLSv1.3;
ssl_early_data on;
location / {
proxy_pass http://app;
proxy_set_header Early-Data $ssl_early_data;
}
}
The app then refuses what it won’t risk:
app.use((req, res, next) => { // Express
if (req.get('Early-Data') === '1' && !['GET', 'HEAD', 'OPTIONS'].includes(req.method)) {
return res.status(425).end();
}
next();
});
Gotchas
- HTTP/3 too. QUIC has 0-RTT, and RFC 9114 requires RFC 8470’s replay protections for HTTP/3.
- Not a rate limit or an outage. That’s 429 or 503; a 425 is about one request’s timing.
- Caching. RFC 8470 says a 425 isn’t cacheable by default, and its body doesn’t represent any resource.
Search 425 429 in the tool below to compare it with Too Many Requests.