Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 16 additions & 14 deletions src/Drivers/LaravelHttpServer.php
Original file line number Diff line number Diff line change
Expand Up @@ -99,12 +99,12 @@ public function start(): void
return;
}

$this->socket = $server = SocketHttpServer::createForDirectAccess(new NullLogger());
$this->socket = $server = SocketHttpServer::createForDirectAccess(new NullLogger);

$server->expose("{$this->host}:{$this->port}");
$server->start(
new ClosureRequestHandler($this->handleRequest(...)),
new DefaultErrorHandler(),
new DefaultErrorHandler,
);
}

Expand Down Expand Up @@ -323,27 +323,29 @@ private function asset(string $filepath): Response
return new Response(404);
}

$mimeTypes = new MimeTypes();
$mimeTypes = new MimeTypes;
$contentType = $mimeTypes->getMimeTypes(pathinfo($filepath, PATHINFO_EXTENSION));

$contentType = $contentType[0] ?? 'application/octet-stream';

if (str_ends_with($filepath, '.js')) {
$temporaryStream = fopen('php://temp', 'r+');
assert($temporaryStream !== false, 'Failed to open temporary stream.');
// Use file_get_contents instead of fread() because fread() is
// limited to 8192 bytes when running inside Amp's event loop.
fclose($file);
$rawContent = file_get_contents($filepath);

// @phpstan-ignore-next-line
$temporaryContent = fread($file, (int) filesize($filepath));
assert($rawContent !== false, 'Failed to read file content.');

assert($temporaryContent !== false, 'Failed to open temporary stream.');
$content = $this->rewriteAssetUrl($rawContent);

$content = $this->rewriteAssetUrl($temporaryContent);
// Build the Response without passing headers to the constructor,
// because the constructor calls setBody() first (which auto-sets
// content-length for string bodies) then setHeaders() (which
// clears ALL headers and only keeps what was passed in).
$response = new Response(200, [], $content);
$response->setHeader('Content-Type', $contentType);

fwrite($temporaryStream, $content);

rewind($temporaryStream);

$file = $temporaryStream;
return $response;
}

return new Response(200, [
Expand Down
54 changes: 54 additions & 0 deletions tests/Unit/Drivers/Laravel/LaravelHttpServerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@

declare(strict_types=1);

use Amp\ByteStream\ReadableBuffer;
use Amp\ByteStream\ReadableResourceStream;
use Illuminate\Http\Request;
use Pest\Browser\Drivers\LaravelHttpServer;

use function Pest\Laravel\withServerVariables;
use function Pest\Laravel\withUnencryptedCookie;
Expand All @@ -21,6 +24,57 @@
->assertDontSee('http://localhost');
});

it('serves JS assets as string body instead of stream', function (): void {
// Regression test: the old implementation used fread() + ReadableResourceStream
// which could truncate JS files at 8192 bytes inside Amp's event loop.
// The fix uses file_get_contents() and returns the content as a string body
// (ReadableBuffer) instead of a stream (ReadableResourceStream).
$path = public_path('string-body-test.js');
@file_put_contents($path, "console.log('ok');");

$server = new LaravelHttpServer('127.0.0.1', 0);

$method = new ReflectionMethod($server, 'asset');

$response = $method->invoke($server, $path);

$body = $response->getBody();

expect($body)->toBeInstanceOf(ReadableBuffer::class)
->and($body)->not->toBeInstanceOf(ReadableResourceStream::class);
});

it('sets content-length header on JS asset responses', function (): void {
// Regression test: the old implementation used ReadableResourceStream which
// caused Response::setBody() to remove the content-length header.
$jsContent = "console.log('content-length test');";
$path = public_path('cl-test.js');
@file_put_contents($path, $jsContent);

$server = new LaravelHttpServer('127.0.0.1', 0);

$method = new ReflectionMethod($server, 'asset');

$response = $method->invoke($server, $path);

expect($response->getHeader('content-length'))->toBe((string) strlen($jsContent))
->and($response->getHeader('content-type'))->toBe('text/javascript');
});

it('serves large JS files completely', function (): void {
// Regression test: fread() can return fewer bytes than requested inside
// Amp's event loop (limited to 8192 per read). Verify files >8KB are served intact.
$padding = str_repeat("// padding line to exceed 8192 bytes\n", 300);
$jsContent = $padding."window.__pest_large_file_marker = 'COMPLETE';";

@file_put_contents(public_path('large.js'), $jsContent);

Route::get('/large-js-test', fn (): string => '<html><body><script src="/large.js"></script></body></html>');

visit('/large-js-test')
->assertScript('window.__pest_large_file_marker', 'COMPLETE');
});

it('includes cookies set in the test', function (): void {
Route::get('/cookies', fn (Request $request): array => $request->cookies->all());

Expand Down