Skip to content

Commit dae024f

Browse files
authored
Merge pull request #292 from clue-labs/happy-request-handler
Improve performance by avoiding unneeded promise wrapping
2 parents b685a2a + 0e29aac commit dae024f

2 files changed

Lines changed: 70 additions & 17 deletions

File tree

src/StreamingServer.php

Lines changed: 32 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,9 @@
1414
use React\Http\Io\MiddlewareRunner;
1515
use React\Http\Io\RequestHeaderParser;
1616
use React\Http\Io\ServerRequest;
17+
use React\Promise;
1718
use React\Promise\CancellablePromiseInterface;
18-
use React\Promise\Promise;
19+
use React\Promise\PromiseInterface;
1920
use React\Socket\ConnectionInterface;
2021
use React\Socket\ServerInterface;
2122
use React\Stream\ReadableStreamInterface;
@@ -229,22 +230,43 @@ public function handleRequest(ConnectionInterface $conn, ServerRequestInterface
229230
$conn->write("HTTP/1.1 100 Continue\r\n\r\n");
230231
}
231232

233+
// execute request handler callback
232234
$callback = $this->callback;
233-
$cancel = null;
234-
$promise = new Promise(function ($resolve, $reject) use ($callback, $request, &$cancel) {
235-
$cancel = $callback($request);
236-
$resolve($cancel);
237-
});
235+
try {
236+
$response = $callback($request);
237+
} catch (\Exception $error) {
238+
// request handler callback throws an Exception
239+
$response = Promise\reject($error);
240+
} catch (\Throwable $error) { // @codeCoverageIgnoreStart
241+
// request handler callback throws a PHP7+ Error
242+
$response = Promise\reject($error); // @codeCoverageIgnoreEnd
243+
}
238244

239245
// cancel pending promise once connection closes
240-
if ($cancel instanceof CancellablePromiseInterface) {
241-
$conn->on('close', function () use ($cancel) {
242-
$cancel->cancel();
246+
if ($response instanceof CancellablePromiseInterface) {
247+
$conn->on('close', function () use ($response) {
248+
$response->cancel();
243249
});
244250
}
245251

252+
// happy path: request body is known to be empty => immediately end stream
253+
if ($contentLength === 0) {
254+
$stream->emit('end');
255+
$stream->close();
256+
}
257+
258+
// happy path: response returned, handle and return immediately
259+
if ($response instanceof ResponseInterface) {
260+
return $this->handleResponse($conn, $request, $response);
261+
}
262+
263+
// did not return a promise? this is an error, convert into one for rejection below.
264+
if (!$response instanceof PromiseInterface) {
265+
$response = Promise\resolve($response);
266+
}
267+
246268
$that = $this;
247-
$promise->then(
269+
$response->then(
248270
function ($response) use ($that, $conn, $request) {
249271
if (!$response instanceof ResponseInterface) {
250272
$message = 'The response callback is expected to resolve with an object implementing Psr\Http\Message\ResponseInterface, but resolved with "%s" instead.';
@@ -272,13 +294,6 @@ function ($error) use ($that, $conn, $request) {
272294
return $that->writeError($conn, 500, $request);
273295
}
274296
);
275-
276-
if ($contentLength === 0) {
277-
// If Body is empty or Content-Length is 0 and won't emit further data,
278-
// 'data' events from other streams won't be called anymore
279-
$stream->emit('end');
280-
$stream->close();
281-
}
282297
}
283298

284299
/** @internal */

tests/FunctionalServerTest.php

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -603,6 +603,44 @@ public function testUpgradeWithThroughStreamReturnsDataAsGiven()
603603
$socket->close();
604604
}
605605

606+
public function testUpgradeWithRequestBodyAndThroughStreamReturnsDataAsGiven()
607+
{
608+
$loop = Factory::create();
609+
$connector = new Connector($loop);
610+
611+
$server = new StreamingServer(function (RequestInterface $request) use ($loop) {
612+
$stream = new ThroughStream();
613+
614+
$loop->addTimer(0.1, function () use ($stream) {
615+
$stream->end();
616+
});
617+
618+
return new Response(101, array('Upgrade' => 'echo'), $stream);
619+
});
620+
621+
$socket = new Socket(0, $loop);
622+
$server->listen($socket);
623+
624+
$result = $connector->connect($socket->getAddress())->then(function (ConnectionInterface $conn) {
625+
$conn->write("POST / HTTP/1.1\r\nHost: example.com:80\r\nUpgrade: echo\r\nContent-Length: 3\r\n\r\n");
626+
$conn->write('hoh');
627+
628+
$conn->once('data', function () use ($conn) {
629+
$conn->write('hello');
630+
$conn->write('world');
631+
});
632+
633+
return Stream\buffer($conn);
634+
});
635+
636+
$response = Block\await($result, $loop, 1.0);
637+
638+
$this->assertStringStartsWith("HTTP/1.1 101 Switching Protocols\r\n", $response);
639+
$this->assertStringEndsWith("\r\n\r\nhelloworld", $response);
640+
641+
$socket->close();
642+
}
643+
606644
public function testConnectWithThroughStreamReturnsDataAsGiven()
607645
{
608646
$loop = Factory::create();

0 commit comments

Comments
 (0)