Skip to content

Commit 25f7136

Browse files
committed
Close inactive connections and requests
This new middleware introduces a timeout of closing inactive connections between requests after a configured amount of seconds. This builds on top of #405 and partially on #422
1 parent 00f3590 commit 25f7136

File tree

8 files changed

+253
-20
lines changed

8 files changed

+253
-20
lines changed

README.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ multiple concurrent HTTP requests without blocking.
7878
* [ServerRequest](#serverrequest)
7979
* [ResponseException](#responseexception)
8080
* [React\Http\Middleware](#reacthttpmiddleware)
81+
* [InactiveConnectionTimeoutMiddleware](#inactiveconnectiontimeoutmiddleware)
8182
* [StreamingRequestMiddleware](#streamingrequestmiddleware)
8283
* [LimitConcurrentRequestsMiddleware](#limitconcurrentrequestsmiddleware)
8384
* [RequestBodyBufferMiddleware](#requestbodybuffermiddleware)
@@ -2630,6 +2631,22 @@ access its underlying response object.
26302631

26312632
### React\Http\Middleware
26322633

2634+
#### InactiveConnectionTimeoutMiddleware
2635+
2636+
The `React\Http\Middleware\InactiveConnectionTimeoutMiddleware` is purely a configuration middleware to configure the
2637+
`HttpServer` to close any inactive connections between requests to close the connection and not leave them needlessly open.
2638+
2639+
The following example configures the `HttpServer` to close any inactive connections after one and a half second:
2640+
2641+
```php
2642+
$http = new React\Http\HttpServer(
2643+
new React\Http\Middleware\InactiveConnectionTimeoutMiddleware(1.5),
2644+
$handler
2645+
);
2646+
```
2647+
> Internally, this class is used as a "value object" to override the default timeout of one minute.
2648+
As such it doesn't have any behavior internally, that is all in the internal "StreamingServer".
2649+
26332650
#### StreamingRequestMiddleware
26342651

26352652
The `React\Http\Middleware\StreamingRequestMiddleware` can be used to

src/HttpServer.php

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,12 @@
55
use Evenement\EventEmitter;
66
use React\EventLoop\Loop;
77
use React\EventLoop\LoopInterface;
8+
use React\Http\Io\Clock;
89
use React\Http\Io\IniUtil;
910
use React\Http\Io\MiddlewareRunner;
11+
use React\Http\Io\RequestHeaderParser;
1012
use React\Http\Io\StreamingServer;
13+
use React\Http\Middleware\InactiveConnectionTimeoutMiddleware;
1114
use React\Http\Middleware\LimitConcurrentRequestsMiddleware;
1215
use React\Http\Middleware\StreamingRequestMiddleware;
1316
use React\Http\Middleware\RequestBodyBufferMiddleware;
@@ -219,10 +222,13 @@ public function __construct($requestHandlerOrLoop)
219222
}
220223

221224
$streaming = false;
225+
$idleConnectTimeout = InactiveConnectionTimeoutMiddleware::DEFAULT_TIMEOUT;
222226
foreach ((array) $requestHandlers as $handler) {
223227
if ($handler instanceof StreamingRequestMiddleware) {
224228
$streaming = true;
225-
break;
229+
}
230+
if ($handler instanceof InactiveConnectionTimeoutMiddleware) {
231+
$idleConnectTimeout = $handler->getTimeout();
226232
}
227233
}
228234

@@ -252,10 +258,11 @@ public function __construct($requestHandlerOrLoop)
252258
* doing anything with the request.
253259
*/
254260
$middleware = \array_filter($middleware, function ($handler) {
255-
return !($handler instanceof StreamingRequestMiddleware);
261+
return !($handler instanceof StreamingRequestMiddleware) && !($handler instanceof InactiveConnectionTimeoutMiddleware);
256262
});
257263

258-
$this->streamingServer = new StreamingServer($loop, new MiddlewareRunner($middleware));
264+
$clock = new Clock($loop);
265+
$this->streamingServer = new StreamingServer(new MiddlewareRunner($middleware), new RequestHeaderParser($loop, $clock, $idleConnectTimeout), $clock);
259266

260267
$that = $this;
261268
$this->streamingServer->on('error', function ($error) use ($that) {

src/Io/RequestHeaderParser.php

Lines changed: 35 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
use Evenement\EventEmitter;
66
use Psr\Http\Message\ServerRequestInterface;
7+
use React\EventLoop\LoopInterface;
78
use React\Http\Message\Response;
89
use React\Http\Message\ServerRequest;
910
use React\Socket\ConnectionInterface;
@@ -24,23 +25,53 @@ class RequestHeaderParser extends EventEmitter
2425
{
2526
private $maxSize = 8192;
2627

28+
/**
29+
* @var LoopInterface
30+
*/
31+
private $loop;
32+
2733
/** @var Clock */
2834
private $clock;
2935

36+
/**
37+
* @var float
38+
*/
39+
private $idleConnectionTimeout;
40+
3041
/** @var array<string|int,array<string,string>> */
3142
private $connectionParams = array();
3243

33-
public function __construct(Clock $clock)
44+
/**
45+
* @param LoopInterface $loop
46+
* @param float $idleConnectionTimeout
47+
*/
48+
public function __construct(LoopInterface $loop, Clock $clock, $idleConnectionTimeout)
3449
{
50+
$this->loop = $loop;
3551
$this->clock = $clock;
52+
$this->idleConnectionTimeout = $idleConnectionTimeout;
3653
}
3754

3855
public function handle(ConnectionInterface $conn)
3956
{
57+
$loop = $this->loop;
58+
$idleConnectionTimeout = $this->idleConnectionTimeout;
59+
$that = $this;
60+
$idleConnectionTimeoutHandler = function () use ($that, $conn) {
61+
$that->emit('error', array(
62+
new \RuntimeException('Request timed out', Response::STATUS_REQUEST_TIMEOUT),
63+
$conn
64+
));
65+
$conn->close();
66+
};
67+
$timer = $loop->addTimer($idleConnectionTimeout, $idleConnectionTimeoutHandler);
68+
$conn->on('close', function () use ($loop, &$timer) {
69+
$loop->cancelTimer($timer);
70+
});
4071
$buffer = '';
4172
$maxSize = $this->maxSize;
42-
$that = $this;
43-
$conn->on('data', $fn = function ($data) use (&$buffer, &$fn, $conn, $maxSize, $that) {
73+
$conn->on('data', $fn = function ($data) use (&$buffer, &$fn, $conn, $maxSize, $that, $loop, &$timer, $idleConnectionTimeout, $idleConnectionTimeoutHandler) {
74+
$loop->cancelTimer($timer);
4475
// append chunk of data to buffer and look for end of request headers
4576
$buffer .= $data;
4677
$endOfHeader = \strpos($buffer, "\r\n\r\n");
@@ -59,6 +90,7 @@ public function handle(ConnectionInterface $conn)
5990

6091
// ignore incomplete requests
6192
if ($endOfHeader === false) {
93+
$timer = $loop->addTimer($idleConnectionTimeout, $idleConnectionTimeoutHandler);
6294
return;
6395
}
6496

src/Io/StreamingServer.php

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
use Evenement\EventEmitter;
66
use Psr\Http\Message\ResponseInterface;
77
use Psr\Http\Message\ServerRequestInterface;
8-
use React\EventLoop\LoopInterface;
98
use React\Http\Message\Response;
109
use React\Http\Message\ServerRequest;
1110
use React\Promise;
@@ -28,7 +27,7 @@
2827
* object in return:
2928
*
3029
* ```php
31-
* $server = new StreamingServer($loop, function (ServerRequestInterface $request) {
30+
* $server = new StreamingServer(function (ServerRequestInterface $request) {
3231
* return new Response(
3332
* Response::STATUS_OK,
3433
* array(
@@ -53,7 +52,7 @@
5352
* in order to start a plaintext HTTP server like this:
5453
*
5554
* ```php
56-
* $server = new StreamingServer($loop, $handler);
55+
* $server = new StreamingServer($handler);
5756
*
5857
* $socket = new React\Socket\SocketServer('0.0.0.0:8080', array(), $loop);
5958
* $server->listen($socket);
@@ -86,6 +85,8 @@ final class StreamingServer extends EventEmitter
8685

8786
/** @var Clock */
8887
private $clock;
88+
private $loop;
89+
private $idleConnectionTimeout;
8990

9091
/**
9192
* Creates an HTTP server that invokes the given callback for each incoming HTTP request
@@ -95,19 +96,19 @@ final class StreamingServer extends EventEmitter
9596
* connections in order to then parse incoming data as HTTP.
9697
* See also [listen()](#listen) for more details.
9798
*
98-
* @param LoopInterface $loop
9999
* @param callable $requestHandler
100+
* @param float $idleConnectTimeout
100101
* @see self::listen()
101102
*/
102-
public function __construct(LoopInterface $loop, $requestHandler)
103+
public function __construct($requestHandler, RequestHeaderParser $parser, Clock $clock)
103104
{
104105
if (!\is_callable($requestHandler)) {
105106
throw new \InvalidArgumentException('Invalid request handler given');
106107
}
107108

108109
$this->callback = $requestHandler;
109-
$this->clock = new Clock($loop);
110-
$this->parser = new RequestHeaderParser($this->clock);
110+
$this->clock = $clock;
111+
$this->parser = $parser;
111112

112113
$that = $this;
113114
$this->parser->on('headers', function (ServerRequestInterface $request, ConnectionInterface $conn) use ($that) {
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
<?php
2+
3+
namespace React\Http\Middleware;
4+
5+
use Psr\Http\Message\ResponseInterface;
6+
use Psr\Http\Message\ServerRequestInterface;
7+
use React\Http\Io\HttpBodyStream;
8+
use React\Http\Io\PauseBufferStream;
9+
use React\Promise;
10+
use React\Promise\PromiseInterface;
11+
use React\Promise\Deferred;
12+
use React\Stream\ReadableStreamInterface;
13+
14+
/**
15+
* Closes any inactive connection after the specified amount of seconds since last activity.
16+
*
17+
* This allows you to set an alternative timeout to the default one minute (60 seconds). For example
18+
* thirteen and a half seconds:
19+
*
20+
* ```php
21+
* $http = new React\Http\HttpServer(
22+
* new React\Http\Middleware\InactiveConnectionTimeoutMiddleware(13.5),
23+
* $handler
24+
* );
25+
*
26+
* > Internally, this class is used as a "value object" to override the default timeout of one minute.
27+
* As such it doesn't have any behavior internally, that is all in the internal "StreamingServer".
28+
*/
29+
final class InactiveConnectionTimeoutMiddleware
30+
{
31+
/**
32+
* @internal
33+
*/
34+
const DEFAULT_TIMEOUT = 60;
35+
36+
/**
37+
* @var float
38+
*/
39+
private $timeout;
40+
41+
/**
42+
* @param float $timeout
43+
*/
44+
public function __construct($timeout = self::DEFAULT_TIMEOUT)
45+
{
46+
$this->timeout = $timeout;
47+
}
48+
49+
public function __invoke(ServerRequestInterface $request, $next)
50+
{
51+
return $next($request);
52+
}
53+
54+
/**
55+
* @return float
56+
* @internal
57+
*/
58+
public function getTimeout()
59+
{
60+
return $this->timeout;
61+
}
62+
}

tests/HttpServerTest.php

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
use React\EventLoop\Loop;
77
use React\Http\HttpServer;
88
use React\Http\Io\IniUtil;
9+
use React\Http\Io\StreamingServer;
10+
use React\Http\Middleware\InactiveConnectionTimeoutMiddleware;
911
use React\Http\Middleware\StreamingRequestMiddleware;
1012
use React\Promise;
1113
use React\Promise\Deferred;
@@ -60,10 +62,18 @@ public function testConstructWithoutLoopAssignsLoopAutomatically()
6062
$ref->setAccessible(true);
6163
$clock = $ref->getValue($streamingServer);
6264

65+
$ref = new \ReflectionProperty($streamingServer, 'parser');
66+
$ref->setAccessible(true);
67+
$parser = $ref->getValue($streamingServer);
68+
6369
$ref = new \ReflectionProperty($clock, 'loop');
6470
$ref->setAccessible(true);
6571
$loop = $ref->getValue($clock);
6672

73+
$ref = new \ReflectionProperty($parser, 'loop');
74+
$ref->setAccessible(true);
75+
$loop = $ref->getValue($parser);
76+
6777
$this->assertInstanceOf('React\EventLoop\LoopInterface', $loop);
6878
}
6979

@@ -257,6 +267,18 @@ function (ServerRequestInterface $request) use (&$streaming) {
257267
$this->assertEquals(true, $streaming);
258268
}
259269

270+
public function testIdleConnectionWillBeClosedAfterConfiguredTimeout()
271+
{
272+
$this->connection->expects($this->once())->method('close');
273+
274+
$http = new HttpServer(Loop::get(), new InactiveConnectionTimeoutMiddleware(0.1), $this->expectCallableNever());
275+
276+
$http->listen($this->socket);
277+
$this->socket->emit('connection', array($this->connection));
278+
279+
Loop::run();
280+
}
281+
260282
public function testForwardErrors()
261283
{
262284
$exception = new \Exception();
@@ -439,7 +461,7 @@ public function testConstructServerWithMemoryLimitDoesLimitConcurrency()
439461

440462
public function testConstructFiltersOutConfigurationMiddlewareBefore()
441463
{
442-
$http = new HttpServer(new StreamingRequestMiddleware(), function () { });
464+
$http = new HttpServer(new InactiveConnectionTimeoutMiddleware(0), new StreamingRequestMiddleware(), function () { });
443465

444466
$ref = new \ReflectionProperty($http, 'streamingServer');
445467
$ref->setAccessible(true);

0 commit comments

Comments
 (0)