From e55137cbab5a6804409dae23983b3538e0823ba7 Mon Sep 17 00:00:00 2001 From: Hafez Divandari Date: Thu, 10 Sep 2026 10:58:13 +0200 Subject: [PATCH 1/4] fix canonical URL handling for subdomain routing --- src/Configuration.php | 7 ++ src/Drivers/LaravelHttpServer.php | 89 +++++++++++++++++++----- tests/Browser/Visit/CanonicalUrlTest.php | 66 ++++++++++++++++++ tests/Browser/Visit/SubdomainTest.php | 10 +-- 4 files changed, 149 insertions(+), 23 deletions(-) create mode 100644 tests/Browser/Visit/CanonicalUrlTest.php diff --git a/src/Configuration.php b/src/Configuration.php index bf3c458f..c1b015f5 100644 --- a/src/Configuration.php +++ b/src/Configuration.php @@ -4,6 +4,7 @@ namespace Pest\Browser; +use Pest\Browser\Drivers\LaravelHttpServer; use Pest\Browser\Enums\BrowserType; use Pest\Browser\Enums\ColorScheme; use Pest\Browser\Playwright\Playwright; @@ -102,6 +103,12 @@ public function withHost(?string $host): self { Playwright::setHost($host); + $http = ServerManager::instance()->http(); + + if ($http instanceof LaravelHttpServer) { + $http->syncCanonicalUrl(); + } + return $this; } diff --git a/src/Drivers/LaravelHttpServer.php b/src/Drivers/LaravelHttpServer.php index ffa12a6c..278c3b70 100644 --- a/src/Drivers/LaravelHttpServer.php +++ b/src/Drivers/LaravelHttpServer.php @@ -70,7 +70,7 @@ public function __destruct() } /** - * Rewrite the given URL to match the server's host and port. + * Rewrite the given URL to match the server's canonical host and port. */ public function rewrite(string $url): string { @@ -85,7 +85,7 @@ public function rewrite(string $url): string $path = $parts['path'] ?? '/'; parse_str($parts['query'] ?? '', $queryParameters); - return (string) Uri::of($this->url()) + return (string) Uri::of($this->canonicalUrl()) ->withPath($path) ->withQuery($queryParameters); } @@ -148,10 +148,6 @@ public function bootstrap(): void { $this->start(); - $url = $this->url(); - - config(['app.url' => $url]); - config(['cors.paths' => ['*']]); if (app()->bound('url')) { @@ -160,10 +156,41 @@ public function bootstrap(): void assert($urlGenerator instanceof UrlGenerator); $this->setOriginalAssetUrl($urlGenerator->asset('')); + $urlGenerator->forceScheme('http'); + } + + $this->syncCanonicalUrl(); + } + + /** + * Re-sync Laravel's `app.url` and the URL generator's origin to the canonical URL, so that `route()`, + * `asset()`, and `config('app.url')` stay consistent with the host the browser is navigating to. + */ + public function syncCanonicalUrl(): void + { + if (! $this->socket instanceof AmpHttpServer) { + return; + } + + // Guard against being called when the Laravel container is not (yet) bootstrapped, + // e.g. from a Pest `beforeAll` hook, or between test files in a parallel worker + // after the previous test's app has been torn down. + if (! app()->bound('config')) { + return; + } + $url = $this->canonicalUrl(); + + config(['app.url' => $url]); + + if (app()->bound('url')) { + $urlGenerator = app('url'); + + assert($urlGenerator instanceof UrlGenerator); + + $urlGenerator->setRequest(Request::create($url)); $urlGenerator->useOrigin($url); $urlGenerator->useAssetOrigin($url); - $urlGenerator->forceScheme('http'); } } @@ -194,7 +221,7 @@ public function throwLastThrowableIfNeeded(): void } /** - * Get the public path for the given path. + * Get the URL of the bound socket (always 127.0.0.1:). */ private function url(): string { @@ -205,6 +232,28 @@ private function url(): string return sprintf('http://%s:%d', $this->host, $this->port); } + /** + * Get the canonical host the user expects URLs to use. + * + * Defaults to the bound IP unless the test configured a host with `withHost(...)`. + */ + private function canonicalHost(): string + { + return Playwright::host() ?? $this->host; + } + + /** + * Get the canonical base URL used for generated links and request URIs. + */ + private function canonicalUrl(): string + { + if (! $this->socket instanceof AmpHttpServer) { + throw new ServerNotFoundException('The HTTP server is not running.'); + } + + return sprintf('http://%s:%d', $this->canonicalHost(), $this->port); + } + /** * Sets the original asset URL. */ @@ -224,11 +273,17 @@ private function handleRequest(AmpRequest $request): Response Execution::instance()->tick(); } + // Re-sync per request as a safety net — if the configured host changes + // mid-test, the URL generator and config should reflect it before the + // app handles the request. + $this->syncCanonicalUrl(); + $canonicalUrl = $this->canonicalUrl(); + $uri = $request->getUri(); $path = in_array($uri->getPath(), ['', '0'], true) ? '/' : $uri->getPath(); $query = $uri->getQuery() ?? ''; // @phpstan-ignore-line $fullPath = $path.($query !== '' ? '?'.$query : ''); - $absoluteUrl = mb_rtrim($this->url(), '/').$fullPath; + $absoluteUrl = mb_rtrim($canonicalUrl, '/').$fullPath; $filepath = public_path($path); if (file_exists($filepath) && ! is_dir($filepath)) { @@ -261,15 +316,13 @@ private function handleRequest(AmpRequest $request): Response $symfonyRequest->headers->add($request->getHeaders()); - // 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); - $symfonyRequest->headers->set('Host', $hostHeader); - // Also set SERVER_NAME for Laravel routing - $symfonyRequest->server->set('SERVER_NAME', $configuredHost); - $symfonyRequest->server->set('HTTP_HOST', $hostHeader); - } + // Ensure the framework sees the canonical host (e.g. for subdomain routing) + // even when the browser arrived via a different network host (e.g. 127.0.0.1). + $canonicalHost = $this->canonicalHost(); + $hostHeader = sprintf('%s:%d', $canonicalHost, $this->port); + $symfonyRequest->headers->set('Host', $hostHeader); + $symfonyRequest->server->set('SERVER_NAME', $canonicalHost); + $symfonyRequest->server->set('HTTP_HOST', $hostHeader); $debug = config('app.debug'); diff --git a/tests/Browser/Visit/CanonicalUrlTest.php b/tests/Browser/Visit/CanonicalUrlTest.php new file mode 100644 index 00000000..f1e78602 --- /dev/null +++ b/tests/Browser/Visit/CanonicalUrlTest.php @@ -0,0 +1,66 @@ +url() when withHost is set', function (): void { + Route::domain('app.localhost')->get('/canonical/request-url', fn (Request $request) => $request->url()); + + pest()->browser()->withHost('app.localhost'); + + $port = ServerManager::instance()->http()->port; // @phpstan-ignore-line + + visit('/canonical/request-url') + ->assertUrlIs("http://app.localhost:{$port}/canonical/request-url") + ->assertSee("http://app.localhost:{$port}/canonical/request-url"); +}); + +it('generates route() URLs against the canonical host when withHost is set', function (): void { + Route::domain('app.localhost')->as('canonical.test')->get('/canonical/route-url', fn (): string => route('canonical.test')); + + pest()->browser()->withHost('app.localhost'); + + $port = ServerManager::instance()->http()->port; // @phpstan-ignore-line + + visit('/canonical/route-url') + ->assertSee("http://app.localhost:{$port}/canonical/route-url"); + + expect(route('canonical.test'))->toBe("http://app.localhost:{$port}/canonical/route-url"); +}); + +it('reverts to the bound IP origin when withHost(null) clears the configured host', function (): void { + Route::as('canonical.no-host')->get('/canonical/no-host', fn (): string => route('canonical.no-host')); + + // Ensure no leaking host from earlier tests. + pest()->browser()->withHost(null); + + $port = ServerManager::instance()->http()->port; // @phpstan-ignore-line + + visit('/canonical/no-host') + ->assertSee("http://127.0.0.1:{$port}/canonical/no-host"); + + expect(route('canonical.no-host'))->toBe("http://127.0.0.1:{$port}/canonical/no-host"); +}); + +it('updates the URL generator immediately when withHost changes mid-test', function (): void { + Route::domain('first.localhost')->as('canonical.first')->get('/canonical/first', fn (): string => route('canonical.first')); + Route::domain('second.localhost')->as('canonical.second')->get('/canonical/second', fn (): string => route('canonical.second')); + + $port = ServerManager::instance()->http()->port; // @phpstan-ignore-line + + visit('/canonical/first') + ->withHost('first.localhost') + ->assertSee("http://first.localhost:{$port}/canonical/first"); + + expect(route('canonical.first'))->toBe("http://first.localhost:{$port}/canonical/first"); + + // Per-request resync should also surface the new host on the page itself. + visit('/canonical/second') + ->withHost('second.localhost') + ->assertSee("http://second.localhost:{$port}/canonical/second"); + + expect(route('canonical.second'))->toBe("http://second.localhost:{$port}/canonical/second"); +}); diff --git a/tests/Browser/Visit/SubdomainTest.php b/tests/Browser/Visit/SubdomainTest.php index bfb33678..192359ac 100644 --- a/tests/Browser/Visit/SubdomainTest.php +++ b/tests/Browser/Visit/SubdomainTest.php @@ -70,18 +70,18 @@ 'host' => request()->getHost(), ]); - // Set global host: test.domain - pest()->browser()->withHost('test.domain'); + // Set global host: test.localhost + pest()->browser()->withHost('test.localhost'); // 1. Visit withHost: api.localhost visit('/api/health') ->withHost('api.localhost') ->assertSee('"host":"api.localhost"') - ->assertDontSee('test.domain'); + ->assertDontSee('test.localhost'); - // 2. Visit without withHost: should use global host "test.domain" + // 2. Visit without withHost: should use global host "test.localhost" visit('/') - ->assertSee('"host":"test.domain"') + ->assertSee('"host":"test.localhost"') ->assertDontSee('api.localhost'); }); From 4e96b48aee7c4d0489c3fe0b489b40e7280054d0 Mon Sep 17 00:00:00 2001 From: Hafez Divandari Date: Thu, 10 Sep 2026 12:05:57 +0200 Subject: [PATCH 2/4] raise Amp body size limit to allow large JSON request bodies --- src/Drivers/LaravelHttpServer.php | 12 ++++++- tests/Browser/Visit/LargeBodyTest.php | 47 +++++++++++++++++++++++++++ 2 files changed, 58 insertions(+), 1 deletion(-) create mode 100644 tests/Browser/Visit/LargeBodyTest.php diff --git a/src/Drivers/LaravelHttpServer.php b/src/Drivers/LaravelHttpServer.php index 278c3b70..a428326f 100644 --- a/src/Drivers/LaravelHttpServer.php +++ b/src/Drivers/LaravelHttpServer.php @@ -7,6 +7,7 @@ use Amp\ByteStream\ReadableResourceStream; use Amp\Http\Cookie\RequestCookie; use Amp\Http\Server\DefaultErrorHandler; +use Amp\Http\Server\Driver\DefaultHttpDriverFactory; use Amp\Http\Server\HttpServer as AmpHttpServer; use Amp\Http\Server\HttpServerStatus; use Amp\Http\Server\Request as AmpRequest; @@ -99,7 +100,16 @@ public function start(): void return; } - $this->socket = $server = SocketHttpServer::createForDirectAccess(new NullLogger()); + // Increase the body size limit beyond Amp's 128-KiB + // default to prevent deadlocks when reading large + // JSON payloads in POST, PUT, and PATCH requests. + $this->socket = $server = SocketHttpServer::createForDirectAccess( + logger: $logger = new NullLogger(), + httpDriverFactory: new DefaultHttpDriverFactory( + logger: $logger, + bodySizeLimit: 64 * 1024 * 1024, + ), + ); $server->expose("{$this->host}:{$this->port}"); $server->start( diff --git a/tests/Browser/Visit/LargeBodyTest.php b/tests/Browser/Visit/LargeBodyTest.php new file mode 100644 index 00000000..fa4faa6f --- /dev/null +++ b/tests/Browser/Visit/LargeBodyTest.php @@ -0,0 +1,47 @@ + [ + 'received_bytes' => mb_strlen($request->getContent()), + 'received_count' => count($request->json('items', [])), + ]); + + // Build a payload that comfortably exceeds Amp's 128 KiB default. + $items = []; + for ($i = 0; $i < 5000; $i++) { + $items[] = [ + 'id' => $i, + 'name' => str_repeat('x', 32), + 'description' => str_repeat('y', 64), + ]; + } + $payload = json_encode(['items' => $items]); + + expect(mb_strlen($payload))->toBeGreaterThan(128 * 1024); + + // Render a tiny page that POSTs the payload via fetch and writes the JSON + // response into the DOM so we can assert against it. + Route::get('/large-body/post-from-page', fn (): string => ' + +

+            
+        
+    ');
+
+    visit('/large-body/post-from-page')
+        ->assertSee('"received_bytes":'.mb_strlen($payload))
+        ->assertSee('"received_count":5000');
+});

From b153845bf362b2173a6f27fe9d44ce0c428289d0 Mon Sep 17 00:00:00 2001
From: Hafez Divandari 
Date: Thu, 10 Sep 2026 14:58:43 +0200
Subject: [PATCH 3/4] address review feedback on canonical url handling

Co-Authored-By: Claude Opus 5 
---
 src/Configuration.php                         |   7 +-
 src/Drivers/LaravelHttpServer.php             | 119 +++++++++---------
 src/ServerManager.php                         |   8 ++
 tests/Browser/Visit/CanonicalUrlTest.php      |  33 ++++-
 tests/Browser/Visit/SubdomainTest.php         |   4 +
 .../Configuration/HostConfigurationTest.php   |  13 ++
 .../Drivers/Laravel/LaravelHttpServerTest.php |   6 +
 7 files changed, 121 insertions(+), 69 deletions(-)

diff --git a/src/Configuration.php b/src/Configuration.php
index c1b015f5..5c6b1fd5 100644
--- a/src/Configuration.php
+++ b/src/Configuration.php
@@ -103,10 +103,11 @@ public function withHost(?string $host): self
     {
         Playwright::setHost($host);
 
-        $http = ServerManager::instance()->http();
+        // If the server is already running, re-point generated URLs at the new host straight away
+        $serverManager = ServerManager::instance();
 
-        if ($http instanceof LaravelHttpServer) {
-            $http->syncCanonicalUrl();
+        if ($serverManager->hasHttp() && ($http = $serverManager->http()) instanceof LaravelHttpServer) {
+            $http->syncGeneratedUrls();
         }
 
         return $this;
diff --git a/src/Drivers/LaravelHttpServer.php b/src/Drivers/LaravelHttpServer.php
index a428326f..beafd030 100644
--- a/src/Drivers/LaravelHttpServer.php
+++ b/src/Drivers/LaravelHttpServer.php
@@ -25,6 +25,7 @@
 use Pest\Browser\Execution;
 use Pest\Browser\GlobalState;
 use Pest\Browser\Playwright\Playwright;
+use Psr\Http\Message\UriInterface;
 use Psr\Log\NullLogger;
 use Symfony\Component\Mime\MimeTypes;
 use Throwable;
@@ -86,7 +87,7 @@ public function rewrite(string $url): string
         $path = $parts['path'] ?? '/';
         parse_str($parts['query'] ?? '', $queryParameters);
 
-        return (string) Uri::of($this->canonicalUrl())
+        return (string) Uri::of($this->url())
             ->withPath($path)
             ->withQuery($queryParameters);
     }
@@ -169,39 +170,7 @@ public function bootstrap(): void
             $urlGenerator->forceScheme('http');
         }
 
-        $this->syncCanonicalUrl();
-    }
-
-    /**
-     * Re-sync Laravel's `app.url` and the URL generator's origin to the canonical URL, so that `route()`,
-     * `asset()`, and `config('app.url')` stay consistent with the host the browser is navigating to.
-     */
-    public function syncCanonicalUrl(): void
-    {
-        if (! $this->socket instanceof AmpHttpServer) {
-            return;
-        }
-
-        // Guard against being called when the Laravel container is not (yet) bootstrapped,
-        // e.g. from a Pest `beforeAll` hook, or between test files in a parallel worker
-        // after the previous test's app has been torn down.
-        if (! app()->bound('config')) {
-            return;
-        }
-
-        $url = $this->canonicalUrl();
-
-        config(['app.url' => $url]);
-
-        if (app()->bound('url')) {
-            $urlGenerator = app('url');
-
-            assert($urlGenerator instanceof UrlGenerator);
-
-            $urlGenerator->setRequest(Request::create($url));
-            $urlGenerator->useOrigin($url);
-            $urlGenerator->useAssetOrigin($url);
-        }
+        $this->syncGeneratedUrls();
     }
 
     /**
@@ -231,37 +200,62 @@ public function throwLastThrowableIfNeeded(): void
     }
 
     /**
-     * Get the URL of the bound socket (always 127.0.0.1:).
+     * Point Laravel's `app.url` and the URL generator's origin at the given URL, so that `route()`,
+     * `asset()` and `config('app.url')` stay consistent with the host the browser is navigating to.
      */
-    private function url(): string
+    public function syncGeneratedUrls(?string $url = null): void
     {
         if (! $this->socket instanceof AmpHttpServer) {
-            throw new ServerNotFoundException('The HTTP server is not running.');
+            return;
         }
 
-        return sprintf('http://%s:%d', $this->host, $this->port);
+        // Guard against being called when the Laravel container is not (yet) bootstrapped,
+        // e.g. from a Pest `beforeAll` hook, or between test files in a parallel worker
+        // after the previous test's app has been torn down.
+        if (! app()->bound('config')) {
+            return;
+        }
+
+        $url ??= $this->url();
+
+        config(['app.url' => $url]);
+
+        if (app()->bound('url')) {
+            $urlGenerator = app('url');
+
+            assert($urlGenerator instanceof UrlGenerator);
+
+            $urlGenerator->setRequest(Request::create($url));
+            $urlGenerator->useOrigin($url);
+            $urlGenerator->useAssetOrigin($url);
+        }
     }
 
     /**
-     * Get the canonical host the user expects URLs to use.
+     * Get the base URL of the server.
      *
-     * Defaults to the bound IP unless the test configured a host with `withHost(...)`.
+     * Uses the host configured with `withHost(...)` when set, so generated links
+     * match the host the browser navigates to, and falls back to the bound IP.
      */
-    private function canonicalHost(): string
+    private function url(): string
     {
-        return Playwright::host() ?? $this->host;
+        if (! $this->socket instanceof AmpHttpServer) {
+            throw new ServerNotFoundException('The HTTP server is not running.');
+        }
+
+        return sprintf('http://%s:%d', Playwright::host() ?? $this->host, $this->port);
     }
 
     /**
-     * Get the canonical base URL used for generated links and request URIs.
+     * Get the origin (scheme, host and port) the given request URI arrived on.
      */
-    private function canonicalUrl(): string
+    private function originOf(UriInterface $uri): string
     {
-        if (! $this->socket instanceof AmpHttpServer) {
-            throw new ServerNotFoundException('The HTTP server is not running.');
+        if ($uri->getHost() === '') {
+            return mb_rtrim($this->url(), '/');
         }
 
-        return sprintf('http://%s:%d', $this->canonicalHost(), $this->port);
+        return sprintf('http://%s:%d', $uri->getHost(), $uri->getPort() ?? $this->port);
     }
 
     /**
@@ -269,7 +263,10 @@ private function canonicalUrl(): string
      */
     private function setOriginalAssetUrl(string $url): void
     {
-        $this->originalAssetUrl = mb_rtrim($url, '/');
+        // Captured once, before any sync re-points the generator's asset origin at this
+        // server. Re-capturing later would record our own origin as the "original" and
+        // stop asset rewriting from matching anything.
+        $this->originalAssetUrl ??= mb_rtrim($url, '/');
     }
 
     /**
@@ -283,17 +280,21 @@ private function handleRequest(AmpRequest $request): Response
             Execution::instance()->tick();
         }
 
-        // Re-sync per request as a safety net — if the configured host changes
-        // mid-test, the URL generator and config should reflect it before the
-        // app handles the request.
-        $this->syncCanonicalUrl();
-        $canonicalUrl = $this->canonicalUrl();
-
         $uri = $request->getUri();
         $path = in_array($uri->getPath(), ['', '0'], true) ? '/' : $uri->getPath();
         $query = $uri->getQuery() ?? ''; // @phpstan-ignore-line
         $fullPath = $path.($query !== '' ? '?'.$query : '');
-        $absoluteUrl = mb_rtrim($canonicalUrl, '/').$fullPath;
+
+        // The browser reaches us on the host the test asked for, so trust the
+        // request's own origin rather than the globally configured one. This
+        // keeps a per-visit `withHost(...)` intact for the page's later
+        // fetch/XHR requests, after the global host has been restored.
+        $origin = $this->originOf($uri);
+        $absoluteUrl = $origin.$fullPath;
+
+        // Keep `route()`, `asset()` and `config('app.url')` aligned with the
+        // origin the app is answering on for this request.
+        $this->syncGeneratedUrls($origin);
 
         $filepath = public_path($path);
         if (file_exists($filepath) && ! is_dir($filepath)) {
@@ -326,14 +327,6 @@ private function handleRequest(AmpRequest $request): Response
 
         $symfonyRequest->headers->add($request->getHeaders());
 
-        // Ensure the framework sees the canonical host (e.g. for subdomain routing)
-        // even when the browser arrived via a different network host (e.g. 127.0.0.1).
-        $canonicalHost = $this->canonicalHost();
-        $hostHeader = sprintf('%s:%d', $canonicalHost, $this->port);
-        $symfonyRequest->headers->set('Host', $hostHeader);
-        $symfonyRequest->server->set('SERVER_NAME', $canonicalHost);
-        $symfonyRequest->server->set('HTTP_HOST', $hostHeader);
-
         $debug = config('app.debug');
 
         try {
diff --git a/src/ServerManager.php b/src/ServerManager.php
index 4e406fd9..6d1af5fb 100644
--- a/src/ServerManager.php
+++ b/src/ServerManager.php
@@ -78,6 +78,14 @@ public function playwright(): PlaywrightServer
         return $this->playwright;
     }
 
+    /**
+     * Determines whether the HTTP server instance has already been created.
+     */
+    public function hasHttp(): bool
+    {
+        return $this->http instanceof HttpServer;
+    }
+
     /**
      * Returns the HTTP server process instance.
      */
diff --git a/tests/Browser/Visit/CanonicalUrlTest.php b/tests/Browser/Visit/CanonicalUrlTest.php
index f1e78602..2ad7316c 100644
--- a/tests/Browser/Visit/CanonicalUrlTest.php
+++ b/tests/Browser/Visit/CanonicalUrlTest.php
@@ -6,6 +6,10 @@
 use Illuminate\Support\Facades\Route;
 use Pest\Browser\ServerManager;
 
+afterEach(function (): void {
+    pest()->browser()->withHost(null);
+});
+
 it('exposes the canonical host on request()->url() when withHost is set', function (): void {
     Route::domain('app.localhost')->get('/canonical/request-url', fn (Request $request) => $request->url());
 
@@ -34,9 +38,6 @@
 it('reverts to the bound IP origin when withHost(null) clears the configured host', function (): void {
     Route::as('canonical.no-host')->get('/canonical/no-host', fn (): string => route('canonical.no-host'));
 
-    // Ensure no leaking host from earlier tests.
-    pest()->browser()->withHost(null);
-
     $port = ServerManager::instance()->http()->port; // @phpstan-ignore-line
 
     visit('/canonical/no-host')
@@ -64,3 +65,29 @@
 
     expect(route('canonical.second'))->toBe("http://second.localhost:{$port}/canonical/second");
 });
+
+it('rewrites asset URLs to the configured host so scripts stay same-origin', function (): void {
+    @file_put_contents(public_path('canonical-asset.js'), "console.log('base http://localhost');");
+
+    pest()->browser()->withHost('assets.localhost');
+
+    visit('/canonical-asset.js')
+        ->assertSee('http://assets.localhost')
+        ->assertDontSee('http://127.0.0.1');
+});
+
+it('updates generated URLs as soon as withHost changes, before the next request', function (): void {
+    Route::as('canonical.plain')->get('/canonical/plain', fn (): string => 'plain');
+
+    $port = ServerManager::instance()->http()->port; // @phpstan-ignore-line
+
+    pest()->browser()->withHost('one.localhost');
+    visit('/canonical/plain')->assertSee('plain');
+
+    expect(route('canonical.plain'))->toBe("http://one.localhost:{$port}/canonical/plain");
+
+    // No request in between: the URL generator should already reflect the new host.
+    pest()->browser()->withHost('two.localhost');
+
+    expect(route('canonical.plain'))->toBe("http://two.localhost:{$port}/canonical/plain");
+});
diff --git a/tests/Browser/Visit/SubdomainTest.php b/tests/Browser/Visit/SubdomainTest.php
index 192359ac..f2a5ad3b 100644
--- a/tests/Browser/Visit/SubdomainTest.php
+++ b/tests/Browser/Visit/SubdomainTest.php
@@ -5,6 +5,10 @@
 use Illuminate\Support\Facades\Route;
 use Pest\Browser\Playwright\Playwright;
 
+afterEach(function (): void {
+    pest()->browser()->withHost(null);
+});
+
 it('can visit non-subdomain routes with subdomain host browser testing', function (): void {
     Route::get('/app-test', fn (): string => '
         
diff --git a/tests/Unit/Configuration/HostConfigurationTest.php b/tests/Unit/Configuration/HostConfigurationTest.php
index a6797ad6..0dea35eb 100644
--- a/tests/Unit/Configuration/HostConfigurationTest.php
+++ b/tests/Unit/Configuration/HostConfigurationTest.php
@@ -4,12 +4,17 @@
 
 use Pest\Browser\Configuration;
 use Pest\Browser\Playwright\Playwright;
+use Pest\Browser\ServerManager;
 
 beforeEach(function (): void {
     // Reset Playwright state before each test
     Playwright::setHost(null);
 });
 
+afterEach(function (): void {
+    Playwright::setHost(null);
+});
+
 it('can set host via configuration', function (): void {
     $config = new Configuration();
 
@@ -64,3 +69,11 @@
         expect(Playwright::host())->toBe($host);
     }
 });
+
+it('does not create the http server when setting a host', function (): void {
+    $hadServer = ServerManager::instance()->hasHttp();
+
+    new Configuration()->withHost('tenant.localhost');
+
+    expect(ServerManager::instance()->hasHttp())->toBe($hadServer);
+});
diff --git a/tests/Unit/Drivers/Laravel/LaravelHttpServerTest.php b/tests/Unit/Drivers/Laravel/LaravelHttpServerTest.php
index 7ca9688e..e3fa739e 100644
--- a/tests/Unit/Drivers/Laravel/LaravelHttpServerTest.php
+++ b/tests/Unit/Drivers/Laravel/LaravelHttpServerTest.php
@@ -7,6 +7,12 @@
 use function Pest\Laravel\withServerVariables;
 use function Pest\Laravel\withUnencryptedCookie;
 
+// These assertions pin the bound IP, so make the host explicit rather than
+// inheriting whatever an earlier test file configured globally.
+beforeEach(function (): void {
+    pest()->browser()->withHost(null);
+});
+
 it('rewrites the URLs on JS files', function (): void {
     @file_put_contents(
         public_path('app.js'),

From efd9b6db54fd59c7ca8a32f15b6bba500b877801 Mon Sep 17 00:00:00 2001
From: Hafez Divandari 
Date: Thu, 10 Sep 2026 14:59:49 +0200
Subject: [PATCH 4/4] Revert "raise Amp body size limit to allow large JSON
 request bodies"

This reverts commit 4e96b48aee7c4d0489c3fe0b489b40e7280054d0.
---
 src/Drivers/LaravelHttpServer.php     | 12 +------
 tests/Browser/Visit/LargeBodyTest.php | 47 ---------------------------
 2 files changed, 1 insertion(+), 58 deletions(-)
 delete mode 100644 tests/Browser/Visit/LargeBodyTest.php

diff --git a/src/Drivers/LaravelHttpServer.php b/src/Drivers/LaravelHttpServer.php
index beafd030..e7f296ec 100644
--- a/src/Drivers/LaravelHttpServer.php
+++ b/src/Drivers/LaravelHttpServer.php
@@ -7,7 +7,6 @@
 use Amp\ByteStream\ReadableResourceStream;
 use Amp\Http\Cookie\RequestCookie;
 use Amp\Http\Server\DefaultErrorHandler;
-use Amp\Http\Server\Driver\DefaultHttpDriverFactory;
 use Amp\Http\Server\HttpServer as AmpHttpServer;
 use Amp\Http\Server\HttpServerStatus;
 use Amp\Http\Server\Request as AmpRequest;
@@ -101,16 +100,7 @@ public function start(): void
             return;
         }
 
-        // Increase the body size limit beyond Amp's 128-KiB
-        // default to prevent deadlocks when reading large
-        // JSON payloads in POST, PUT, and PATCH requests.
-        $this->socket = $server = SocketHttpServer::createForDirectAccess(
-            logger: $logger = new NullLogger(),
-            httpDriverFactory: new DefaultHttpDriverFactory(
-                logger: $logger,
-                bodySizeLimit: 64 * 1024 * 1024,
-            ),
-        );
+        $this->socket = $server = SocketHttpServer::createForDirectAccess(new NullLogger());
 
         $server->expose("{$this->host}:{$this->port}");
         $server->start(
diff --git a/tests/Browser/Visit/LargeBodyTest.php b/tests/Browser/Visit/LargeBodyTest.php
deleted file mode 100644
index fa4faa6f..00000000
--- a/tests/Browser/Visit/LargeBodyTest.php
+++ /dev/null
@@ -1,47 +0,0 @@
- [
-        'received_bytes' => mb_strlen($request->getContent()),
-        'received_count' => count($request->json('items', [])),
-    ]);
-
-    // Build a payload that comfortably exceeds Amp's 128 KiB default.
-    $items = [];
-    for ($i = 0; $i < 5000; $i++) {
-        $items[] = [
-            'id' => $i,
-            'name' => str_repeat('x', 32),
-            'description' => str_repeat('y', 64),
-        ];
-    }
-    $payload = json_encode(['items' => $items]);
-
-    expect(mb_strlen($payload))->toBeGreaterThan(128 * 1024);
-
-    // Render a tiny page that POSTs the payload via fetch and writes the JSON
-    // response into the DOM so we can assert against it.
-    Route::get('/large-body/post-from-page', fn (): string => '
-        
-            

-            
-        
-    ');
-
-    visit('/large-body/post-from-page')
-        ->assertSee('"received_bytes":'.mb_strlen($payload))
-        ->assertSee('"received_count":5000');
-});