From f4d76c7a0fe1cd77c1b951a31c92eaad3d69ae60 Mon Sep 17 00:00:00 2001 From: G4Zz0L1 Date: Sat, 13 Jun 2026 15:15:12 +0200 Subject: [PATCH] fix: bind HTTP server to an OS-assigned ephemeral port Avoids the find-then-rebind TOCTOU in Port::find() that intermittently throws 'Address already in use' when many parallel test workers each start a LaravelHttpServer. Bind to port 0 and resolve the actual bound port from getServers() after start. --- src/Drivers/LaravelHttpServer.php | 24 ++++++++++++++++++++++-- src/ServerManager.php | 4 +++- 2 files changed, 25 insertions(+), 3 deletions(-) diff --git a/src/Drivers/LaravelHttpServer.php b/src/Drivers/LaravelHttpServer.php index 97ae5fdb..600f61ff 100644 --- a/src/Drivers/LaravelHttpServer.php +++ b/src/Drivers/LaravelHttpServer.php @@ -13,6 +13,7 @@ use Amp\Http\Server\RequestHandler\ClosureRequestHandler; use Amp\Http\Server\Response; use Amp\Http\Server\SocketHttpServer; +use Amp\Socket\InternetAddress; use Illuminate\Contracts\Debug\ExceptionHandler; use Illuminate\Contracts\Http\Kernel as HttpKernel; use Illuminate\Foundation\Testing\Concerns\WithoutExceptionHandlingHandler; @@ -50,6 +51,15 @@ final class LaravelHttpServer implements HttpServer */ private ?Throwable $lastThrowable = null; + /** + * The port the server actually bound to, resolved at start time. + * + * When constructed with port 0 the operating system assigns a free + * ephemeral port atomically at bind time, which avoids the find-then-rebind + * race that can throw "Address already in use" under parallel test runs. + */ + private ?int $resolvedPort = null; + /** * Creates a new laravel http server instance. */ @@ -106,6 +116,16 @@ public function start(): void new ClosureRequestHandler($this->handleRequest(...)), new DefaultErrorHandler(), ); + + foreach ($server->getServers() as $socketServer) { + $address = $socketServer->getAddress(); + + if ($address instanceof InternetAddress) { + $this->resolvedPort = $address->getPort(); + + break; + } + } } /** @@ -202,7 +222,7 @@ private function url(): string throw new ServerNotFoundException('The HTTP server is not running.'); } - return sprintf('http://%s:%d', $this->host, $this->port); + return sprintf('http://%s:%d', $this->host, $this->resolvedPort ?? $this->port); } /** @@ -264,7 +284,7 @@ private function handleRequest(AmpRequest $request): Response // Set the Host header to match the configured host for subdomain routing $configuredHost = Playwright::host(); if ($configuredHost !== null) { - $hostHeader = sprintf('%s:%d', $configuredHost, $this->port); + $hostHeader = sprintf('%s:%d', $configuredHost, $this->resolvedPort ?? $this->port); $symfonyRequest->headers->set('Host', $hostHeader); // Also set SERVER_NAME for Laravel routing $symfonyRequest->server->set('SERVER_NAME', $configuredHost); diff --git a/src/ServerManager.php b/src/ServerManager.php index 4e406fd9..8545f9fb 100644 --- a/src/ServerManager.php +++ b/src/ServerManager.php @@ -86,7 +86,9 @@ public function http(): HttpServer return $this->http ??= match (function_exists('app_path')) { true => new LaravelHttpServer( self::DEFAULT_HOST, // Always bind to 127.0.0.1 for server - Port::find(), + 0, // Bind to an OS-assigned ephemeral port at bind time; the server + // resolves the actual port after starting. Avoids the find-then-rebind + // race that throws "Address already in use" under parallel runs. ), default => new NullableHttpServer(), };