fix(http): advertise keep-alive on HTTP/1.0 responses we keep open
1.0 defaults to close: honoring 'Connection: Keep-Alive' without echoing it back left spec-following clients waiting for an EOF that never came (ab -k deadlocked to its poll timeout on response 1). As agreed: - serialise_response echoes 'connection: keep-alive' when keep_alive is on AND version is 1.0. 1.1 keep-alive stays implicit. - Error statuses (>=400) on 1.0 force keep_alive off in the conn actor (joins the existing 1.0-stream force-close): we don't advertise reuse on a 4xx/5xx to a protocol generation where reuse is opt-in. - HEAD needs nothing: the echo is framing-neutral. - Parse-error responses already carried 'connection: close'. Tests: unit pair (1.0 echo / 1.1 implicit) + integration pair (socket actually reused on 1.0; 404 forces close + EOF). Validated against the original repro: ab -k -n 5000 -c 50 GET /users -> 5000/5000 keep-alive, 0 failed, ~63k rps (~18.9k without keep-alive).
This commit is contained in:
@@ -660,6 +660,51 @@ fn keep_alive_after_chunked_response() {
|
||||
assert_eq!(http_body(&resp), b"hello");
|
||||
}
|
||||
|
||||
/// HTTP/1.0 keep-alive must be advertised back (`connection: keep-alive`)
|
||||
/// when honored — 1.0 defaults to close, so a silent keep-open leaves
|
||||
/// spec-following clients (ab -k) waiting for EOF. The socket then serves
|
||||
/// a second request.
|
||||
#[test]
|
||||
fn http10_keepalive_advertised_and_socket_reused() {
|
||||
let pipe = Pipeline::new().plug(
|
||||
Router::new().get("/", |c: Conn, _n: Next| c.put_status(200))
|
||||
);
|
||||
let port = spawn_server(pipe);
|
||||
let mut s = TcpStream::connect(("127.0.0.1", port)).unwrap();
|
||||
s.set_read_timeout(Some(Duration::from_secs(5))).unwrap();
|
||||
|
||||
let req = b"GET / HTTP/1.0\r\nHost: x\r\nConnection: keep-alive\r\n\r\n";
|
||||
s.write_all(req).unwrap();
|
||||
let head = String::from_utf8(read_response_head(&mut s)).unwrap();
|
||||
assert!(head.starts_with("HTTP/1.0 200 OK\r\n"), "head: {head}");
|
||||
assert!(head.contains("connection: keep-alive"), "head: {head}");
|
||||
|
||||
// Same socket, second request — the keep-alive was real.
|
||||
s.write_all(req).unwrap();
|
||||
let head2 = String::from_utf8(read_response_head(&mut s)).unwrap();
|
||||
assert!(head2.starts_with("HTTP/1.0 200 OK\r\n"), "head: {head2}");
|
||||
}
|
||||
|
||||
/// Error statuses on HTTP/1.0 force close even when the client asked for
|
||||
/// keep-alive: `connection: close` on the wire, then EOF.
|
||||
#[test]
|
||||
fn http10_keepalive_forced_off_on_error_status() {
|
||||
let pipe = Pipeline::new().plug(
|
||||
Router::new().get("/", |c: Conn, _n: Next| c.put_status(200))
|
||||
);
|
||||
let port = spawn_server(pipe);
|
||||
let mut s = TcpStream::connect(("127.0.0.1", port)).unwrap();
|
||||
s.set_read_timeout(Some(Duration::from_secs(5))).unwrap();
|
||||
|
||||
s.write_all(b"GET /missing HTTP/1.0\r\nHost: x\r\nConnection: keep-alive\r\n\r\n")
|
||||
.unwrap();
|
||||
let mut resp = Vec::new();
|
||||
s.read_to_end(&mut resp).unwrap(); // EOF: server closed
|
||||
let head = String::from_utf8_lossy(&resp).to_string();
|
||||
assert!(head.starts_with("HTTP/1.0 404 Not Found\r\n"), "head: {head}");
|
||||
assert!(head.contains("connection: close"), "head: {head}");
|
||||
}
|
||||
|
||||
/// HTTP/1.0 has no chunked framing: a stream body goes out raw, the
|
||||
/// response carries `connection: close` and no transfer-encoding, and EOF
|
||||
/// delimits the body.
|
||||
|
||||
Reference in New Issue
Block a user