Skip to content

Commit 4df9953

Browse files
asonixrobjtedeJohnTitor
authored
actix-http: h1: stop pipelining when not reading full requests (#3721)
* actix-http: h1: stop pipelining when not reading full requests The existing pipelining behavior of the h1 dispatcher can cause client timeouts if the entire request body isn't read. It puts the dispatcher into a state where it refuses to read more (payload dropped) but there are still bytes in the buffer from the request body. This solution adds the SHUTDOWN flag in addition to the FINISHED flag when completing a response when both the following are true: 1. There are no messages in `this.messages` 2. There is still a payload in `this.payload` This combination implies two things. First, that we have not parsed a pipelined request after the request we have just responded to. Second, that the current request payload has not been fed an EOF. Because there are no pipelined requests, we know that the current request payload belongs to the request we have just responded to, and because the request payload has not been fed an EOF, we know we never finished reading it. When this occurs, adding the SHUTDOWN flag to the dispatcher triggers a `flush` and a `poll_shutdown` on the IO resource on the next poll. * Remove printlns from dispatcher * Add test that fails without changes & passes with changes * Add changelog entry for h1 shutdown --------- Co-authored-by: Rob Ede <[email protected]> Co-authored-by: Yuki Okushi <[email protected]>
1 parent 0168411 commit 4df9953

File tree

3 files changed

+111
-5
lines changed

3 files changed

+111
-5
lines changed

actix-http/CHANGES.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
- Properly wake Payload receivers when feeding errors or EOF.
66
- Add `ServiceConfigBuilder` type to facilitate future configuration extensions.
77
- Add a configuration option to allow/disallow half closed connections in HTTP/1. This defaults to allow, reverting the change made in 3.11.1.
8+
- Shutdown connections when HTTP Responses are written without reading full Requests.
89

910
## 3.11.1
1011

actix-http/src/h1/dispatcher.rs

Lines changed: 43 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -386,7 +386,14 @@ where
386386
let mut this = self.project();
387387
this.state.set(match size {
388388
BodySize::None | BodySize::Sized(0) => {
389-
this.flags.insert(Flags::FINISHED);
389+
let payload_unfinished = this.payload.is_some();
390+
391+
if payload_unfinished {
392+
this.flags.insert(Flags::SHUTDOWN | Flags::FINISHED);
393+
} else {
394+
this.flags.insert(Flags::FINISHED);
395+
}
396+
390397
State::None
391398
}
392399
_ => State::SendPayload { body },
@@ -404,7 +411,14 @@ where
404411
let mut this = self.project();
405412
this.state.set(match size {
406413
BodySize::None | BodySize::Sized(0) => {
407-
this.flags.insert(Flags::FINISHED);
414+
let payload_unfinished = this.payload.is_some();
415+
416+
if payload_unfinished {
417+
this.flags.insert(Flags::SHUTDOWN | Flags::FINISHED);
418+
} else {
419+
this.flags.insert(Flags::FINISHED);
420+
}
421+
408422
State::None
409423
}
410424
_ => State::SendErrorPayload { body },
@@ -503,10 +517,22 @@ where
503517
Poll::Ready(None) => {
504518
this.codec.encode(Message::Chunk(None), this.write_buf)?;
505519

520+
// if we have not yet pipelined to the next request, then
521+
// this.payload was the payload for the request we just finished
522+
// responding to. We can check to see if we finished reading it
523+
// yet, and if not, shutdown the connection.
524+
let payload_unfinished = this.payload.is_some();
525+
let not_pipelined = this.messages.is_empty();
526+
506527
// payload stream finished.
507528
// set state to None and handle next message
508529
this.state.set(State::None);
509-
this.flags.insert(Flags::FINISHED);
530+
531+
if not_pipelined && payload_unfinished {
532+
this.flags.insert(Flags::SHUTDOWN | Flags::FINISHED);
533+
} else {
534+
this.flags.insert(Flags::FINISHED);
535+
}
510536

511537
continue 'res;
512538
}
@@ -542,10 +568,22 @@ where
542568
Poll::Ready(None) => {
543569
this.codec.encode(Message::Chunk(None), this.write_buf)?;
544570

545-
// payload stream finished
571+
// if we have not yet pipelined to the next request, then
572+
// this.payload was the payload for the request we just finished
573+
// responding to. We can check to see if we finished reading it
574+
// yet, and if not, shutdown the connection.
575+
let payload_unfinished = this.payload.is_some();
576+
let not_pipelined = this.messages.is_empty();
577+
578+
// payload stream finished.
546579
// set state to None and handle next message
547580
this.state.set(State::None);
548-
this.flags.insert(Flags::FINISHED);
581+
582+
if not_pipelined && payload_unfinished {
583+
this.flags.insert(Flags::SHUTDOWN | Flags::FINISHED);
584+
} else {
585+
this.flags.insert(Flags::FINISHED);
586+
}
549587

550588
continue 'res;
551589
}

actix-http/src/h1/dispatcher_tests.rs

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -535,6 +535,73 @@ async fn pipelining_ok_then_ok() {
535535
.await;
536536
}
537537

538+
#[actix_rt::test]
539+
async fn early_response_with_payload_closes_connection() {
540+
lazy(|cx| {
541+
let buf = TestBuffer::new(
542+
"\
543+
GET /unfinished HTTP/1.1\r\n\
544+
Content-Length: 2\r\n\
545+
\r\n\
546+
",
547+
);
548+
549+
let cfg = ServiceConfig::new(
550+
KeepAlive::Os,
551+
Duration::from_millis(1),
552+
Duration::from_millis(1),
553+
false,
554+
None,
555+
);
556+
557+
let services = HttpFlow::new(echo_path_service(), ExpectHandler, None);
558+
559+
let h1 = Dispatcher::<_, _, _, _, UpgradeHandler>::new(
560+
buf.clone(),
561+
services,
562+
cfg,
563+
None,
564+
OnConnectData::default(),
565+
);
566+
567+
pin!(h1);
568+
569+
assert!(matches!(&h1.inner, DispatcherState::Normal { .. }));
570+
571+
match h1.as_mut().poll(cx) {
572+
Poll::Pending => panic!("Should have shut down"),
573+
Poll::Ready(res) => assert!(res.is_ok()),
574+
}
575+
576+
// polls: initial => shutdown
577+
assert_eq!(h1.poll_count, 2);
578+
579+
{
580+
let mut res = buf.write_buf_slice_mut();
581+
stabilize_date_header(&mut res);
582+
let res = &res[..];
583+
584+
let exp = b"\
585+
HTTP/1.1 200 OK\r\n\
586+
content-length: 11\r\n\
587+
date: Thu, 01 Jan 1970 12:34:56 UTC\r\n\r\n\
588+
/unfinished\
589+
";
590+
591+
assert_eq!(
592+
res,
593+
exp,
594+
"\nexpected response not in write buffer:\n\
595+
response: {:?}\n\
596+
expected: {:?}",
597+
String::from_utf8_lossy(res),
598+
String::from_utf8_lossy(exp)
599+
);
600+
}
601+
})
602+
.await;
603+
}
604+
538605
#[actix_rt::test]
539606
async fn pipelining_ok_then_bad() {
540607
lazy(|cx| {

0 commit comments

Comments
 (0)