Skip to content

Commit 027bd02

Browse files
committed
Cancel request reading upon shutdown
Fixes #367 and #370.
1 parent c74232a commit 027bd02

File tree

2 files changed

+68
-10
lines changed

2 files changed

+68
-10
lines changed

src/Driver/Http1Driver.php

+32-10
Original file line numberDiff line numberDiff line change
@@ -100,9 +100,10 @@ public function handleClient(
100100
$this->insertTimeout();
101101

102102
$headerSizeLimit = $this->headerSizeLimit;
103+
$cancellation = $this->deferredCancellation->getCancellation();
103104

104105
try {
105-
$buffer = $readableStream->read();
106+
$buffer = $readableStream->read($cancellation);
106107
if ($buffer === null) {
107108
$this->removeTimeout();
108109
return;
@@ -141,7 +142,7 @@ public function handleClient(
141142
);
142143
}
143144

144-
$chunk = $readableStream->read();
145+
$chunk = $readableStream->read($cancellation);
145146
if ($chunk === null) {
146147
return;
147148
}
@@ -413,7 +414,8 @@ public function handleClient(
413414
$this->suspendTimeout();
414415

415416
$this->currentBuffer = $buffer;
416-
$this->handleRequest($request);
417+
$this->pendingResponse = async($this->handleRequest(...), $request);
418+
$this->pendingResponse->await();
417419
$this->pendingResponseCount--;
418420

419421
continue;
@@ -486,7 +488,7 @@ static function (int $bodySize) use (&$bodySizeLimit): void {
486488
);
487489
}
488490

489-
$chunk = $this->readableStream->read();
491+
$chunk = $this->readableStream->read($cancellation);
490492
if ($chunk === null) {
491493
return;
492494
}
@@ -514,7 +516,7 @@ static function (int $bodySize) use (&$bodySizeLimit): void {
514516

515517
if ($chunkLengthRemaining === 0) {
516518
while (!isset($buffer[1])) {
517-
$chunk = $readableStream->read();
519+
$chunk = $readableStream->read($cancellation);
518520
if ($chunk === null) {
519521
return;
520522
}
@@ -546,7 +548,7 @@ static function (int $bodySize) use (&$bodySizeLimit): void {
546548
);
547549
}
548550

549-
$chunk = $this->readableStream->read();
551+
$chunk = $this->readableStream->read($cancellation);
550552
if ($chunk === null) {
551553
return;
552554
}
@@ -599,7 +601,7 @@ static function (int $bodySize) use (&$bodySizeLimit): void {
599601
$remaining -= $bodyBufferSize;
600602
}
601603

602-
$body = $readableStream->read();
604+
$body = $readableStream->read($cancellation);
603605
if ($body === null) {
604606
return;
605607
}
@@ -635,7 +637,7 @@ static function (int $bodySize) use (&$bodySizeLimit): void {
635637
$bufferLength = \strlen($buffer);
636638

637639
if (!$bufferLength) {
638-
$chunk = $readableStream->read();
640+
$chunk = $readableStream->read($cancellation);
639641
if ($chunk === null) {
640642
return;
641643
}
@@ -647,7 +649,7 @@ static function (int $bodySize) use (&$bodySizeLimit): void {
647649
// These first two (extreme) edge cases prevent errors where the packet boundary ends after
648650
// the \r and before the \n at the end of a chunk.
649651
if ($bufferLength === $chunkLengthRemaining || $bufferLength === $chunkLengthRemaining + 1) {
650-
$chunk = $readableStream->read();
652+
$chunk = $readableStream->read($cancellation);
651653
if ($chunk === null) {
652654
return;
653655
}
@@ -704,7 +706,7 @@ static function (int $bodySize) use (&$bodySizeLimit): void {
704706
$bodySize += $bodyBufferSize;
705707
}
706708

707-
$chunk = $readableStream->read();
709+
$chunk = $readableStream->read($cancellation);
708710
if ($chunk === null) {
709711
return;
710712
}
@@ -756,6 +758,12 @@ static function (int $bodySize) use (&$bodySizeLimit): void {
756758
}
757759
} catch (StreamException) {
758760
// Client disconnected, finally block will clean up.
761+
} catch (CancelledException) {
762+
// Server shutting down.
763+
if ($this->bodyQueue === null || !$this->pendingResponseCount) {
764+
// Send a service unavailable response only if another response has not already been sent.
765+
$this->sendServiceUnavailableResponse($request ?? null)->await();
766+
}
759767
} finally {
760768
$this->pendingResponse->finally(function (): void {
761769
$this->removeTimeout();
@@ -1023,6 +1031,19 @@ private function upgrade(Request $request, Response $response): void
10231031
}
10241032
}
10251033

1034+
/**
1035+
* Creates a service unavailable response from the error handler and sends that response to the client.
1036+
*
1037+
* @return Future<void>
1038+
*/
1039+
private function sendServiceUnavailableResponse(?Request $request): Future
1040+
{
1041+
$response = $this->errorHandler->handleError(HttpStatus::SERVICE_UNAVAILABLE, request: $request);
1042+
$response->setHeader("connection", "close");
1043+
1044+
return $this->lastWrite = async($this->send(...), $this->lastWrite, $response);
1045+
}
1046+
10261047
/**
10271048
* Creates an error response from the error handler and sends that response to the client.
10281049
*
@@ -1062,6 +1083,7 @@ public function stop(): void
10621083

10631084
$this->pendingResponse->await();
10641085
$this->lastWrite?->await();
1086+
$this->deferredCancellation->cancel();
10651087
}
10661088

10671089
public function getApplicationLayerProtocols(): array

test/Driver/Http1DriverTest.php

+36
Original file line numberDiff line numberDiff line change
@@ -1209,4 +1209,40 @@ public function testTimeoutSuspendedDuringRequestHandler(): void
12091209

12101210
self::assertStringStartsWith('HTTP/1.1 202', $output->buffer());
12111211
}
1212+
1213+
public function testShutdownDuringRequestRead(): void
1214+
{
1215+
$driver = new Http1Driver(
1216+
new ClosureRequestHandler(fn () => self::fail('Request handler not expected to be called')),
1217+
$this->createMock(ErrorHandler::class),
1218+
new NullLogger,
1219+
);
1220+
1221+
$input = new Queue();
1222+
$input->pushAsync(
1223+
// Insufficient request headers
1224+
"POST /post HTTP/1.1\r\n" .
1225+
"Host: localhost\r\n" .
1226+
"Content-Length: 100\r\n"
1227+
);
1228+
1229+
$output = new WritableBuffer();
1230+
1231+
async(fn () => $driver->handleClient(
1232+
$this->createClientMock(),
1233+
new ReadableIterableStream($input->iterate()),
1234+
$output,
1235+
));
1236+
1237+
delay(0.1); // Allow parser generator to run.
1238+
1239+
$driver->stop();
1240+
1241+
delay(0.1); // Give time for cancellation to be processed.
1242+
1243+
$input->complete();
1244+
$output->close();
1245+
1246+
self::assertStringStartsWith('HTTP/1.0 503', $output->buffer());
1247+
}
12121248
}

0 commit comments

Comments
 (0)