Skip to content

Commit

Permalink
Merge pull request #207 from kesselb/handle-connection-abort-with-ign…
Browse files Browse the repository at this point in the history
…ore-user-abort

fix: handle client disconnect properly with ignore_user_abort true
  • Loading branch information
phil-davis authored Jun 26, 2023
2 parents c2cd0a5 + c574923 commit 97c8184
Show file tree
Hide file tree
Showing 3 changed files with 106 additions and 0 deletions.
6 changes: 6 additions & 0 deletions lib/Sapi.php
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,12 @@ public static function sendResponse(ResponseInterface $response): void
if ($copied <= 0) {
break;
}
// Abort on client disconnect.
// With ignore_user_abort(true), the script is not aborted on client disconnect.
// To avoid reading the entire stream and dismissing the data afterward, check between the chunks if the client is still there.
if (1 === ignore_user_abort() && 1 === connection_aborted()) {
break;
}
$left -= $copied;
}
} else {
Expand Down
31 changes: 31 additions & 0 deletions tests/HTTP/SapiTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -300,4 +300,35 @@ public function testSendWorksWithCallbackAsBody(): void

self::assertEquals('foo', $result);
}

public function testSendConnectionAborted(): void
{
$baseUrl = getenv('BASEURL');
if (!$baseUrl) {
$this->markTestSkipped('Set an environment value BASEURL to continue');
}

$url = rtrim($baseUrl, '/').'/connection_aborted.php';
$chunk_size = 4 * 1024 * 1024;
$fetch_size = 6 * 1024 * 1024;

$stream = fopen($url, 'r');
$size = 0;

while ($size <= $fetch_size) {
$temp = fread($stream, 8192);
if (false === $temp) {
break;
}
$size += strlen($temp);
}

fclose($stream);

sleep(5);

$bytes_read = file_get_contents(sys_get_temp_dir().'/dummy_stream_read_counter');
$this->assertEquals($chunk_size * 2, $bytes_read);
$this->assertGreaterThanOrEqual($fetch_size, $bytes_read);
}
}
69 changes: 69 additions & 0 deletions tests/www/connection_aborted.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
<?php

declare(strict_types=1);

use Sabre\HTTP;

include '../bootstrap.php';

class DummyStream
{
private int $position;

public function stream_open(string $path, string $mode, int $options, ?string &$opened_path): bool
{
$this->position = 0;

return true;
}

public function stream_read(int $count): string
{
$this->position += $count;

return random_bytes($count);
}

public function stream_tell(): int
{
return $this->position;
}

public function stream_eof(): bool
{
return $this->position > 25 * 1024 * 1024;
}

public function stream_close(): void
{
file_put_contents(sys_get_temp_dir().'/dummy_stream_read_counter', $this->position);
}
}

/*
* The DummyStream wrapper has two functions:
* - Provide dummy data.
* - Count how many bytes have been read.
*/
stream_wrapper_register('dummy', DummyStream::class);

/*
* Overwrite default connection handling.
* The default behaviour is however for your script to be aborted when the remote client disconnects.
*
* Nextcloud/ownCloud set ignore_user_abort(true) on purpose to work around
* some edge cases where the default behavior would end a script too early.
*
* https://github.com/owncloud/core/issues/22370
* https://github.com/owncloud/core/pull/26775
*/
ignore_user_abort(true);

$body = fopen('dummy://hello', 'r');

$response = new HTTP\Response();
$response->setStatus(200);
$response->addHeader('Content-Length', 25 * 1024 * 1024);
$response->setBody($body);

HTTP\Sapi::sendResponse($response);

0 comments on commit 97c8184

Please sign in to comment.