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:
+5
-2
@@ -248,10 +248,13 @@ pub fn run_connection(
|
||||
// ----- 4. Write the response. -----
|
||||
// A Stream body on HTTP/1.0 has no chunked framing: the body is
|
||||
// delimited by EOF, so keep-alive is forced off for this response
|
||||
// (and `connection: close` goes on the wire).
|
||||
// (and `connection: close` goes on the wire). Error statuses on
|
||||
// 1.0 also force close: we don't advertise keep-alive on a
|
||||
// 4xx/5xx to a protocol generation where reuse is opt-in.
|
||||
let is_stream = matches!(response_conn.resp_body, RespBody::Stream(_));
|
||||
let is_error = response_conn.status.unwrap_or(200) >= 400;
|
||||
let keep_alive = keep_alive
|
||||
&& !(is_stream && version == HttpVersion::Http10);
|
||||
&& !(version == HttpVersion::Http10 && (is_stream || is_error));
|
||||
|
||||
let head_bytes = parser::serialise_response(&response_conn, keep_alive);
|
||||
if write_all(raw, &head_bytes, Instant::now() + limits.write_timeout).is_err() {
|
||||
|
||||
+28
-2
@@ -252,8 +252,17 @@ pub fn serialise_response(conn: &Conn, keep_alive: bool) -> Vec<u8> {
|
||||
out.extend_from_slice(b"\r\n");
|
||||
}
|
||||
|
||||
if !wrote_connection && !keep_alive {
|
||||
out.extend_from_slice(b"connection: close\r\n");
|
||||
if !wrote_connection {
|
||||
if !keep_alive {
|
||||
out.extend_from_slice(b"connection: close\r\n");
|
||||
} else if conn.version == HttpVersion::Http10 {
|
||||
// HTTP/1.0 defaults to close: a connection we intend to keep
|
||||
// open MUST be advertised back, or a spec-following client
|
||||
// waits for an EOF that never comes (`ab -k` deadlocked on
|
||||
// exactly this). 1.1 keep-alive is the default and stays
|
||||
// implicit.
|
||||
out.extend_from_slice(b"connection: keep-alive\r\n");
|
||||
}
|
||||
}
|
||||
|
||||
out.extend_from_slice(b"\r\n");
|
||||
@@ -452,6 +461,23 @@ mod tests {
|
||||
assert!(s.contains("connection: close"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn serialise_http10_keepalive_is_echoed() {
|
||||
let mut conn = Conn::new().put_status(200).put_body("hi");
|
||||
conn.version = HttpVersion::Http10;
|
||||
let bytes = serialise_response(&conn, true);
|
||||
let s = std::str::from_utf8(&bytes).unwrap();
|
||||
assert!(s.contains("connection: keep-alive"), "head: {s}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn serialise_http11_keepalive_stays_implicit() {
|
||||
let conn = Conn::new().put_status(200).put_body("hi");
|
||||
let bytes = serialise_response(&conn, true);
|
||||
let s = std::str::from_utf8(&bytes).unwrap();
|
||||
assert!(!s.contains("connection:"), "head: {s}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn serialise_user_content_length_is_respected() {
|
||||
let conn = Conn::new().put_status(200)
|
||||
|
||||
Reference in New Issue
Block a user