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
204 changes: 128 additions & 76 deletions src/Http/Http.php
Original file line number Diff line number Diff line change
Expand Up @@ -637,6 +637,53 @@ public function match(Request $request, bool $fresh = false): ?Route
return $this->route;
}

/**
* Capture the current route context so nested dispatches can restore it.
*
* @return array<string, mixed>
*/
private function captureRouteContext(): array
{
return [
'route' => $this->route,
'hasRouteResource' => \array_key_exists('route', $this->resources),
'routeResource' => $this->resources['route'] ?? null,
'hasRouteResourceCallback' => \array_key_exists('route', self::$resourcesCallbacks),
'routeResourceCallback' => self::$resourcesCallbacks['route'] ?? null,
];
}

private function applyRouteContext(?Route $route, Request $request): void
{
$this->route = $route;

self::setResource('route', function () use ($route, $request) {
return $route ?? new Route($request->getMethod(), $request->getURI());
});
}

/**
* Restore the previous route context after nested dispatch completes.
*
* @param array<string, mixed> $context
*/
private function restoreRouteContext(array $context): void
{
$this->route = $context['route'];

if ($context['hasRouteResource']) {
$this->resources['route'] = $context['routeResource'];
} else {
unset($this->resources['route']);
}

if ($context['hasRouteResourceCallback']) {
self::$resourcesCallbacks['route'] = $context['routeResourceCallback'];
} else {
unset(self::$resourcesCallbacks['route']);
}
}

/**
* Execute a given route with middlewares and error handling
*
Expand Down Expand Up @@ -914,86 +961,102 @@ private function runInternal(Request $request, Response $response): static
return $this;
}

$method = $request->getMethod();
$route = $this->match($request);
$this->matchedRoute = $route;
$groups = ($route instanceof Route) ? $route->getGroups() : [];

if (null === $route && null !== self::$wildcardRoute) {
$route = self::$wildcardRoute;
$this->route = $route;
$path = \parse_url($request->getURI(), PHP_URL_PATH);
$route->path($path);
}
$routeContext = $this->captureRouteContext();
$matchedRoute = $this->matchedRoute;

self::setResource('route', function () use ($route, $request) {
return $route ?? new Route($request->getMethod(), $request->getURI());
});
try {
$method = $request->getMethod();
$route = $this->match($request, fresh: true);
$this->matchedRoute = $route;
$groups = ($route instanceof Route) ? $route->getGroups() : [];

if (null === $route && null !== self::$wildcardRoute) {
$route = self::$wildcardRoute;
$path = \parse_url($request->getURI(), PHP_URL_PATH);
$route->path($path);
}

if (self::REQUEST_METHOD_HEAD == $method) {
$method = self::REQUEST_METHOD_GET;
$response->disablePayload();
}
$this->applyRouteContext($route, $request);

if (self::REQUEST_METHOD_OPTIONS == $method) {
try {
foreach ($groups as $group) {
foreach (self::$options as $option) { // Group options hooks
/** @var Hook $option */
if (in_array($group, $option->getGroups())) {
\call_user_func_array($option->getAction(), $this->getArguments($option, [], $request->getParams()));
if (self::REQUEST_METHOD_HEAD == $method) {
$method = self::REQUEST_METHOD_GET;
$response->disablePayload();
}

if (self::REQUEST_METHOD_OPTIONS == $method) {
try {
foreach ($groups as $group) {
foreach (self::$options as $option) { // Group options hooks
/** @var Hook $option */
if (in_array($group, $option->getGroups())) {
\call_user_func_array($option->getAction(), $this->getArguments($option, [], $request->getParams()));
}
}
}
}

foreach (self::$options as $option) { // Global options hooks
/** @var Hook $option */
if (in_array('*', $option->getGroups())) {
\call_user_func_array($option->getAction(), $this->getArguments($option, [], $request->getParams()));
foreach (self::$options as $option) { // Global options hooks
/** @var Hook $option */
if (in_array('*', $option->getGroups())) {
\call_user_func_array($option->getAction(), $this->getArguments($option, [], $request->getParams()));
}
}
}
} catch (\Throwable $e) {
foreach (self::$errors as $error) { // Global error hooks
/** @var Hook $error */
if (in_array('*', $error->getGroups())) {
self::setResource('error', function () use ($e) {
return $e;
});
try {
$arguments = $this->getArguments($error, [], $request->getParams());
\call_user_func_array($error->getAction(), $arguments);
} catch (\Throwable $e) {
throw new Exception('Error handler had an error: ' . $e->getMessage() . "\nStack trace: " . $e->getTraceAsString(), 500, $e);
} catch (\Throwable $e) {
foreach (self::$errors as $error) { // Global error hooks
/** @var Hook $error */
if (in_array('*', $error->getGroups())) {
self::setResource('error', function () use ($e) {
return $e;
});
try {
$arguments = $this->getArguments($error, [], $request->getParams());
\call_user_func_array($error->getAction(), $arguments);
} catch (\Throwable $e) {
throw new Exception('Error handler had an error: ' . $e->getMessage() . "\nStack trace: " . $e->getTraceAsString(), 500, $e);
}
}
}
}

return $this;
}

return $this;
}
if (null !== $route) {
return $this->execute($route, $request, $response);
} elseif (self::REQUEST_METHOD_OPTIONS == $method) {
try {
foreach ($groups as $group) {
foreach (self::$options as $option) { // Group options hooks
if (in_array($group, $option->getGroups())) {
\call_user_func_array($option->getAction(), $this->getArguments($option, [], $request->getParams()));
}
}
}

if (null !== $route) {
return $this->execute($route, $request, $response);
} elseif (self::REQUEST_METHOD_OPTIONS == $method) {
try {
foreach ($groups as $group) {
foreach (self::$options as $option) { // Group options hooks
if (in_array($group, $option->getGroups())) {
foreach (self::$options as $option) { // Global options hooks
if (in_array('*', $option->getGroups())) {
\call_user_func_array($option->getAction(), $this->getArguments($option, [], $request->getParams()));
}
}
}

foreach (self::$options as $option) { // Global options hooks
if (in_array('*', $option->getGroups())) {
\call_user_func_array($option->getAction(), $this->getArguments($option, [], $request->getParams()));
} catch (\Throwable $e) {
foreach (self::$errors as $error) { // Global error hooks
if (in_array('*', $error->getGroups())) {
self::setResource('error', function () use ($e) {
return $e;
});
try {
$arguments = $this->getArguments($error, [], $request->getParams());
\call_user_func_array($error->getAction(), $arguments);
} catch (\Throwable $e) {
throw new Exception('Error handler had an error: ' . $e->getMessage() . "\nStack trace: " . $e->getTraceAsString(), 500, $e);
}
}
}
}
} catch (\Throwable $e) {
} else {
foreach (self::$errors as $error) { // Global error hooks
if (in_array('*', $error->getGroups())) {
self::setResource('error', function () use ($e) {
return $e;
self::setResource('error', function () {
return new Exception('Not Found', 404);
});
try {
$arguments = $this->getArguments($error, [], $request->getParams());
Expand All @@ -1004,23 +1067,12 @@ private function runInternal(Request $request, Response $response): static
}
}
}
} else {
foreach (self::$errors as $error) { // Global error hooks
if (in_array('*', $error->getGroups())) {
self::setResource('error', function () {
return new Exception('Not Found', 404);
});
try {
$arguments = $this->getArguments($error, [], $request->getParams());
\call_user_func_array($error->getAction(), $arguments);
} catch (\Throwable $e) {
throw new Exception('Error handler had an error: ' . $e->getMessage() . "\nStack trace: " . $e->getTraceAsString(), 500, $e);
}
}
}
}

return $this;
return $this;
} finally {
$this->restoreRouteContext($routeContext);
$this->matchedRoute = $matchedRoute;
}
}

/**
Expand Down
64 changes: 64 additions & 0 deletions tests/HttpTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -577,6 +577,70 @@ public function testRouteInjectionAvailableInError(): void
$this->assertSame('GET', $headers['X-Error-Method'] ?? null);
}

public function testNestedInternalDispatchRestoresRouteContext(): void
{
$request = (new Request())
->setMethod('GET')
->setURI('/outer-route');

Http::error()
->inject('utopia')
->inject('route')
->inject('response')
->action(function (Http $app, Route $route, Response $response) {
$response
->addHeader('X-Inner-Error-Route', $route->getPath())
->addHeader('X-Inner-Error-Current-Route', $app->getRoute()?->getPath());
});

Http::get('/inner-route')
->inject('utopia')
->inject('route')
->inject('response')
->action(function (Http $app, Route $route, Response $response) {
$response
->addHeader('X-Inner-Route', $route->getPath())
->addHeader('X-Inner-Current-Route', $app->getRoute()?->getPath());

throw new Exception('Inner route failure');
});

Http::get('/outer-route')
->inject('utopia')
->inject('route')
->inject('response')
->action(function (Http $app, Route $route, Response $response) {
$response
->addHeader('X-Outer-Before-Route', $route->getPath())
->addHeader('X-Outer-Before-Current-Route', $app->getRoute()?->getPath())
->addHeader('X-Outer-Before-Resource-Route', $app->getResource('route')->getPath());

$innerRequest = (new Request())
->setMethod('GET')
->setURI('/inner-route');

$app->run($innerRequest, $response);

$response
->addHeader('X-Outer-After-Current-Route', $app->getRoute()?->getPath())
->addHeader('X-Outer-After-Resource-Route', $app->getResource('route')->getPath());
});

$response = new Response();
$this->app->run($request, $response);
$headers = $response->getHeaders();

$this->assertSame('/outer-route', $headers['X-Outer-Before-Route'] ?? null);
$this->assertSame('/outer-route', $headers['X-Outer-Before-Current-Route'] ?? null);
$this->assertSame('/outer-route', $headers['X-Outer-Before-Resource-Route'] ?? null);
$this->assertSame('/inner-route', $headers['X-Inner-Route'] ?? null);
$this->assertSame('/inner-route', $headers['X-Inner-Current-Route'] ?? null);
$this->assertSame('/inner-route', $headers['X-Inner-Error-Route'] ?? null);
$this->assertSame('/inner-route', $headers['X-Inner-Error-Current-Route'] ?? null);
$this->assertSame('/outer-route', $headers['X-Outer-After-Current-Route'] ?? null);
$this->assertSame('/outer-route', $headers['X-Outer-After-Resource-Route'] ?? null);
}

public function providerRouteMatching(): array
{
return [
Expand Down