From 3f3692976f8c6fed2f35d298244b7ae8f95a6ff9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotr=20Ha=C5=82as?= Date: Sun, 12 Apr 2026 13:34:53 +0200 Subject: [PATCH 1/3] =?UTF-8?q?fix:=20StreamedBinaryFileResponse=20?= =?UTF-8?q?=E2=80=94=20incorrect=20chunking=20logic?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Response/StreamedBinaryFileResponse.php | 21 ++++++++----------- 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/src/Protocol/Http/Response/StreamedBinaryFileResponse.php b/src/Protocol/Http/Response/StreamedBinaryFileResponse.php index 4bc70a5..306681c 100644 --- a/src/Protocol/Http/Response/StreamedBinaryFileResponse.php +++ b/src/Protocol/Http/Response/StreamedBinaryFileResponse.php @@ -34,20 +34,17 @@ public function streamContent(): \Generator $length = $this->maxlen; while ($length && !$file->eof()) { - $read = $length > $this->chunkSize || 0 > $length ? $this->chunkSize : $length; - - if (false === $data = $file->fread($read)) { + $read = ($length > $this->chunkSize || 0 > $length) ? $this->chunkSize : $length; + $data = $file->fread($read); + if ($data === false || $data === '') { + break; + } + yield $data; + if (connection_aborted() !== 0) { break; } - while ('' !== $data) { - yield $data; - if (connection_aborted() !== 0) { - break 2; - } - if (0 < $length) { - $length -= $read; - } - $data = substr($data, $read); + if (0 < $length) { + $length -= strlen($data); } } } finally { From aa57fb7cd094b656988548be7914bb7ea7578b49 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotr=20Ha=C5=82as?= Date: Sun, 12 Apr 2026 14:02:13 +0200 Subject: [PATCH 2/3] changelog: add entry for StreamedBinaryFileResponse fix (#27) --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4def3dc..c8748b7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Files remain accessible via `$request->files` as before - **Migration**: If your code relies on reading raw multipart body via `getContent()`, you'll need to adapt it +- `StreamedBinaryFileResponse::streamContent()` simplified chunking logic + - Removed redundant inner while loop that was effectively a no-op + - Fixed length calculation to use actual data length (`strlen($data)`) instead of requested bytes (`$read`) + - Fixes incorrect chunk count for files where fread() returns fewer bytes than requested + - Fixes (#27) + ## [0.13.0] - 2026-04-05 ### Added From 984f097faf80458ac5698952c4ca5648b5e9d36d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotr=20Ha=C5=82as?= Date: Sun, 12 Apr 2026 14:24:55 +0200 Subject: [PATCH 3/3] =?UTF-8?q?fix:=20Middleware=20Pipeline=20=E2=80=94=20?= =?UTF-8?q?Closure=20Captures=20Wrong=20Request=20(closes=20#21)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Http/HttpRequestHandler.php | 2 +- tests/MiddlewarePipelineTest.php | 150 +++++++++++++++++++++++++++++++ 2 files changed, 151 insertions(+), 1 deletion(-) create mode 100644 tests/MiddlewarePipelineTest.php diff --git a/src/Http/HttpRequestHandler.php b/src/Http/HttpRequestHandler.php index cbbe4e0..42bb78c 100644 --- a/src/Http/HttpRequestHandler.php +++ b/src/Http/HttpRequestHandler.php @@ -53,7 +53,7 @@ public function __invoke(TcpConnection $connection, Request $request): void $next = $this->controller; foreach (array_reverse($this->middlewares) as $middleware) { - $next = fn(Request $input): Http\Response => $middleware($request, $next); + $next = fn(Request $input): Http\Response => $middleware($input, $next); } $response = $next($request); diff --git a/tests/MiddlewarePipelineTest.php b/tests/MiddlewarePipelineTest.php new file mode 100644 index 0000000..21fef47 --- /dev/null +++ b/tests/MiddlewarePipelineTest.php @@ -0,0 +1,150 @@ +executeMiddlewarePipeline([$firstMiddleware, $secondMiddleware]); + + self::assertSame('value-from-first', $tracker->get('first_header')); + self::assertTrue($tracker->get('second_saw_first')); + self::assertSame('value-from-first', $tracker->get('second_first_value')); + } + + public function testThreeMiddlewarePipelinePassesModifiedRequest(): void + { + $tracker = new MiddlewareTracker(); + + $middleware1 = new AddHeaderMiddleware('X-Header-1', 'value-1', $tracker, null); + $middleware2 = new ReadAndAddHeaderMiddleware('x-header-1', $tracker, 'm2_header1', 'X-Header-2', 'value-2'); + $middleware3 = new ReadTwoHeadersMiddleware('x-header-1', 'x-header-2', $tracker, 'm3_header1', 'm3_header2'); + + $this->executeMiddlewarePipeline([$middleware1, $middleware2, $middleware3]); + + self::assertSame('value-1', $tracker->get('m2_header1')); + self::assertSame('value-1', $tracker->get('m3_header1')); + self::assertSame('value-2', $tracker->get('m3_header2')); + } + + /** + * @param MiddlewareInterface[] $middlewares + */ + private function executeMiddlewarePipeline(array $middlewares): void + { + $finalHandler = static fn(Request $request): WorkermanResponse => new WorkermanResponse(200, [], 'Final'); + + $next = $finalHandler; + foreach (array_reverse($middlewares) as $middleware) { + $next = static fn(Request $input) => $middleware($input, $next); + } + + $request = new Request("GET /test HTTP/1.1\r\nHost: localhost\r\n\r\n"); + $next($request); + } +} + +final class MiddlewareTracker +{ + /** @var array */ + private array $data = []; + + public function set(string $key, mixed $value): void + { + $this->data[$key] = $value; + } + + public function get(string $key): mixed + { + return $this->data[$key] ?? null; + } +} + +final readonly class AddHeaderMiddleware implements MiddlewareInterface +{ + public function __construct( + private string $header, + private string $value, + private MiddlewareTracker $tracker, + private ?string $trackKey, + ) { + } + + public function __invoke(Request $request, callable $next): WorkermanResponse + { + $request->withHeader($this->header, $this->value); + if ($this->trackKey !== null) { + $this->tracker->set($this->trackKey, $request->header(strtolower($this->header))); + } + return $next($request); + } +} + +final readonly class ReadHeaderMiddleware implements MiddlewareInterface +{ + public function __construct( + private string $headerName, + private MiddlewareTracker $tracker, + private string $sawKey, + private string $valueKey, + ) { + } + + public function __invoke(Request $request, callable $next): WorkermanResponse + { + $this->tracker->set($this->sawKey, $request->header($this->headerName) !== null); + $this->tracker->set($this->valueKey, $request->header($this->headerName)); + return $next($request); + } +} + +final readonly class ReadAndAddHeaderMiddleware implements MiddlewareInterface +{ + public function __construct( + private string $readHeaderName, + private MiddlewareTracker $tracker, + private string $trackKey, + private string $addHeader, + private string $addValue, + ) { + } + + public function __invoke(Request $request, callable $next): WorkermanResponse + { + $this->tracker->set($this->trackKey, $request->header($this->readHeaderName)); + $request->withHeader($this->addHeader, $this->addValue); + return $next($request); + } +} + +final readonly class ReadTwoHeadersMiddleware implements MiddlewareInterface +{ + public function __construct( + private string $header1, + private string $header2, + private MiddlewareTracker $tracker, + private string $key1, + private string $key2, + ) { + } + + public function __invoke(Request $request, callable $next): WorkermanResponse + { + $this->tracker->set($this->key1, $request->header($this->header1)); + $this->tracker->set($this->key2, $request->header($this->header2)); + return $next($request); + } +}