Skip to content

Commit 30e1405

Browse files
loks0nclaude
andcommitted
Save/restore context['route'] across execute() dispatch
match() no longer writes to context — that was leaking the inner match into the outer's context whenever execute() was called for a sub-request, breaking outer-request shutdown hooks doing ->inject('route') and the http.route telemetry attribute. execute() now sets context['route'] right before dispatching and restores the prior value (or null) in a finally clause, so nested execute() calls don't trample each other's frame. Adds testSubrequestRestoresOuterRoute as a regression test. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent df82079 commit 30e1405

2 files changed

Lines changed: 38 additions & 10 deletions

File tree

src/Http/Http.php

Lines changed: 7 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -517,16 +517,7 @@ public function match(Request $request): ?RouteMatch
517517
$method = $request->getMethod();
518518
$method = (self::REQUEST_METHOD_HEAD === $method) ? self::REQUEST_METHOD_GET : $method;
519519

520-
$match = Router::match($method, $url);
521-
522-
if ($match === null) {
523-
return null;
524-
}
525-
526-
$route = $match->route;
527-
$this->context()->set('route', fn() => $route, []);
528-
529-
return $match;
520+
return Router::match($method, $url);
530521
}
531522

532523
/**
@@ -600,6 +591,10 @@ public function execute(Request $request, Response $response): static
600591
$arguments = [];
601592
$groups = $route->getGroups();
602593

594+
$context = $this->context();
595+
$priorRoute = $context->has('route') ? $context->get('route') : null;
596+
$context->set('route', fn() => $route, []);
597+
603598
try {
604599
if ($route->getHook()) {
605600
foreach (self::$init as $hook) { // Global init hooks
@@ -667,6 +662,8 @@ public function execute(Request $request, Response $response): static
667662
}
668663
}
669664
}
665+
} finally {
666+
$context->set('route', fn() => $priorRoute, []);
670667
}
671668

672669
return $this;

tests/HttpTest.php

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -710,6 +710,37 @@ public function testCanRunRequest(): void
710710
$this->assertStringNotContainsString('HELLO', $result);
711711
}
712712

713+
public function testSubrequestRestoresOuterRoute(): void
714+
{
715+
$_SERVER['REQUEST_METHOD'] = 'GET';
716+
717+
$captured = [];
718+
719+
Http::shutdown()
720+
->inject('route')
721+
->action(function (Route $route) use (&$captured) {
722+
$captured[] = $route->getPath();
723+
});
724+
725+
Http::get('/inner')->action(function () {
726+
// no-op handler — only here so the inner dispatch matches
727+
});
728+
729+
Http::get('/outer')->action(function () {
730+
$inner = new Request();
731+
$inner->setMethod('GET');
732+
$inner->setURI('/inner');
733+
$this->http->execute($inner, new Response());
734+
});
735+
736+
$_SERVER['REQUEST_URI'] = '/outer';
737+
$this->http->execute(new Request(), new Response());
738+
739+
// Inner's shutdown fires first (with inner route), then outer's
740+
// shutdown — which must see the outer route, not the inner one.
741+
$this->assertEquals(['/inner', '/outer'], $captured);
742+
}
743+
713744
public function testWildcardRoute(): void
714745
{
715746
$method = $_SERVER['REQUEST_METHOD'] ?? null;

0 commit comments

Comments
 (0)