From 14ec31b135788dac522d35f1735e67e2023b26ba Mon Sep 17 00:00:00 2001 From: Philip Iezzi Date: Fri, 17 Jul 2026 19:11:35 +0200 Subject: [PATCH] fix: flush scoped() container bindings per request in LaravelHttpServer MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit LaravelHttpServer reuses one long-lived container for every request of a browser test, but never calls Application::forgetScopedInstances() — the only thing that releases scoped() bindings. So a scoped() service resolved in one request leaks into the next and behaves like a singleton, unlike every real runtime: FPM builds a fresh container per request, Octane flushes scoped instances between requests, and even the queue worker does so between jobs. Symptoms look like flaky, order-dependent tests (stale user/team context, wrong locale) and bite hardest right after an auth change mid-test such as an impersonation. Flush scoped instances at the start of each request, before Kernel::handle(), mirroring Octane. --- src/Drivers/LaravelHttpServer.php | 4 ++++ .../Unit/Drivers/Laravel/LaravelHttpServerTest.php | 14 ++++++++++++++ 2 files changed, 18 insertions(+) diff --git a/src/Drivers/LaravelHttpServer.php b/src/Drivers/LaravelHttpServer.php index 97ae5fdb..bbb06565 100644 --- a/src/Drivers/LaravelHttpServer.php +++ b/src/Drivers/LaravelHttpServer.php @@ -273,6 +273,10 @@ private function handleRequest(AmpRequest $request): Response $debug = config('app.debug'); + // This server reuses one container across a test's requests, so scoped() bindings must be + // released per request (as FPM and Octane do) — otherwise they leak like singletons. + app()->forgetScopedInstances(); + try { config(['app.debug' => false]); diff --git a/tests/Unit/Drivers/Laravel/LaravelHttpServerTest.php b/tests/Unit/Drivers/Laravel/LaravelHttpServerTest.php index 7491d6a6..f875d787 100644 --- a/tests/Unit/Drivers/Laravel/LaravelHttpServerTest.php +++ b/tests/Unit/Drivers/Laravel/LaravelHttpServerTest.php @@ -36,3 +36,17 @@ visit('/server-variables') ->assertSee('"test-server-key":"test value"'); }); + +it('flushes scoped container bindings between requests', function (): void { + // A scoped() binding is released only by Application::forgetScopedInstances(). Since this + // server reuses one container across a test's requests, it must flush per request — otherwise + // the instance resolved in the first request leaks into the second (and would behave like a + // singleton), unlike FPM or Octane. Each request renders the resolved object's id; they differ. + app()->scoped('scoped-probe', fn (): object => new stdClass); + Route::get('/scoped-probe', fn (): string => spl_object_hash(app('scoped-probe'))); + + $first = visit('/scoped-probe')->content(); + $second = visit('/scoped-probe')->content(); + + expect($first)->not->toBe($second); +});