Skip to content

Commit 655ea5c

Browse files
committed
Improve sending large chunks over TLS on macOS
See reactphp/stream#150.
1 parent b867505 commit 655ea5c

File tree

3 files changed

+54
-16
lines changed

3 files changed

+54
-16
lines changed

.travis.yml

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
sudo: false
1+
os: linux
22

33
language: php
44

@@ -10,9 +10,17 @@ php:
1010
- 7.4
1111
- nightly
1212

13-
matrix:
13+
jobs:
14+
include:
15+
- name: macOS
16+
os: osx
17+
language: php
18+
before_install:
19+
- curl -s http://getcomposer.org/installer | php
20+
- mv composer.phar /usr/local/bin/composer
1421
allow_failures:
1522
- php: nightly
23+
- os: osx
1624
fast_finish: true
1725

1826
env:

lib/ResourceOutputStream.php

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,9 @@
1313
*/
1414
final class ResourceOutputStream implements OutputStream
1515
{
16+
/** @deprecated No longer used. */
1617
const MAX_CONSECUTIVE_EMPTY_WRITES = 3;
18+
1719
const LARGE_CHUNK_SIZE = 128 * 1024;
1820

1921
/** @var resource|null */
@@ -58,8 +60,6 @@ public function __construct($stream, int $chunkSize = null)
5860
$resource = &$this->resource;
5961

6062
$this->watcher = Loop::onWritable($stream, static function ($watcher, $stream) use ($writes, &$chunkSize, &$writable, &$resource) {
61-
static $emptyWrites = 0;
62-
6363
try {
6464
while (!$writes->isEmpty()) {
6565
/** @var Deferred $deferred */
@@ -75,6 +75,11 @@ public function __construct($stream, int $chunkSize = null)
7575
throw new ClosedException("The stream was closed by the peer");
7676
}
7777

78+
$error = null;
79+
\set_error_handler(static function (int $errno) use (&$error) {
80+
$error = $errno;
81+
});
82+
7883
// Error reporting suppressed since fwrite() emits E_WARNING if the pipe is broken or the buffer is full.
7984
// Use conditional, because PHP doesn't like getting null passed
8085
if ($chunkSize) {
@@ -83,6 +88,8 @@ public function __construct($stream, int $chunkSize = null)
8388
$written = @\fwrite($stream, $data);
8489
}
8590

91+
\restore_error_handler();
92+
8693
\assert(
8794
$written !== false || \PHP_VERSION_ID >= 70400, // PHP 7.4+ returns false on EPIPE.
8895
"Trying to write on a previously fclose()'d resource. Do NOT manually fclose() resources the still referenced in the loop."
@@ -98,21 +105,14 @@ public function __construct($stream, int $chunkSize = null)
98105
}
99106

100107
// Broken pipes between processes on macOS/FreeBSD do not detect EOF properly.
101-
if ($written === 0 || $written === false) {
102-
if ($emptyWrites++ > self::MAX_CONSECUTIVE_EMPTY_WRITES) {
103-
$message = "Failed to write to stream after multiple attempts";
104-
if ($error = \error_get_last()) {
105-
$message .= \sprintf("; %s", $error["message"]);
106-
}
107-
throw new StreamException($message);
108+
if (($written === 0 || $written === false) && $error !== null) {
109+
$message = "Failed to write to stream";
110+
if ($error = \error_get_last()) {
111+
$message .= \sprintf("; %s", $error["message"]);
108112
}
109-
110-
$writes->unshift([$data, $previous, $deferred]);
111-
return;
113+
throw new StreamException($message);
112114
}
113115

114-
$emptyWrites = 0;
115-
116116
if ($length > $written) {
117117
$data = \substr($data, $written);
118118
$writes->unshift([$data, $written + $previous, $deferred]);

test/ResourceOutputStreamTest.php

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,10 @@
22

33
namespace Amp\ByteStream\Test;
44

5+
use Amp\ByteStream\ResourceInputStream;
56
use Amp\ByteStream\ResourceOutputStream;
67
use Amp\ByteStream\StreamException;
8+
use Amp\Delayed;
79
use Amp\PHPUnit\AsyncTestCase;
810

911
class ResourceOutputStreamTest extends AsyncTestCase
@@ -68,6 +70,34 @@ public function testClosedRemoteSocket()
6870

6971
// The first write still succeeds somehow...
7072
yield $stream->write("foobar");
73+
74+
// A delay seems required for the OS to realize the socket is indeed closed.
75+
yield new Delayed(10);
76+
7177
yield $stream->write("foobar");
7278
}
79+
80+
/**
81+
* @see https://github.com/reactphp/stream/pull/150
82+
*/
83+
public function testUploadBiggerBlockSecure()
84+
{
85+
$size = 2 ** 18; // 256kb
86+
87+
$resource = \stream_socket_client('tls://httpbin.org:443');
88+
89+
$output = new ResourceOutputStream($resource);
90+
91+
$body = \str_repeat('.', $size);
92+
93+
yield $output->write("POST /post HTTP/1.0\r\nHost: httpbin.org\r\nContent-Length: $size\r\n\r\n" . $body);
94+
95+
$input = new ResourceInputStream($resource);
96+
$buffer = '';
97+
while (null !== ($chunk = yield $input->read())) {
98+
$buffer .= $chunk;
99+
}
100+
101+
$this->assertStringContainsString($body, $buffer);
102+
}
73103
}

0 commit comments

Comments
 (0)