Skip to content

v5 - Use strict routing#3454

Merged
odan merged 3 commits into
slimphp:5.xfrom
odan:v5-strict-routing
Jun 13, 2026
Merged

v5 - Use strict routing#3454
odan merged 3 commits into
slimphp:5.xfrom
odan:v5-strict-routing

Conversation

@odan

@odan odan commented Jun 13, 2026

Copy link
Copy Markdown
Contributor

/foo and /foo/ are now treated as different routes.

Removed

  • Remove route path normalization

Removed the normalizePath() method and all path normalization from the Router. Route paths are now used exactly as provided by the caller, with no leading-slash prepending, trailing-slash stripping, or double-slash collapsing.

Route defined as Request URI Result
'/foo' /foo 200
'/foo' /foo/ 404 (not found)
'/foo/' /foo/ 200
'/foo' /FOO 404 (not found)
'foo' (no leading slash) /foo 404 (not found)

Trailing slash handling with strict routing

This section explains how to handle trailing slashes depending on your use case.

Option 1: Accept both with a single definition (FastRoute optional segments)

Use FastRoute's optional segment syntax [/] to define a single route that matches both:

$app->get('/foo[/]', function (ServerRequestInterface $request, ResponseInterface $response) {
    $response->getBody()->write('Matches both /foo and /foo/');
    return $response;
});

Option 2: Define both routes explicitly

If you want both /foo and /foo/ to work with different handlers:

$app->get('/foo', function (ServerRequestInterface $request, ResponseInterface $response) {
    $response->getBody()->write('Handler for /foo');
    return $response;
});

$app->get('/foo/', function (ServerRequestInterface $request, ResponseInterface $response) {
    $response->getBody()->write('Handler for /foo/');
    return $response;
});

Option 3: Redirect or rewrite trailing slashes via middleware

For GET requests a permanent redirect (301) is fine. For other request methods like POST or PUT the browser will change the second request to GET. To avoid this, remove the trailing slash silently and pass the rewritten URI to the next handler.

use Psr\Http\Message\ResponseFactoryInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;

final class TrailingSlashMiddleware implements MiddlewareInterface
{
    private ResponseFactoryInterface $responseFactory;

    public function __construct(ResponseFactoryInterface $responseFactory)
    {
        $this->responseFactory = $responseFactory;
    }

    public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
    {
        $uri = $request->getUri();
        $path = $uri->getPath();

        if ($path !== '/' && str_ends_with($path, '/')) {
            // Recursively remove trailing slashes
            $path = rtrim($path, '/');
            $uri = $uri->withPath($path);

            if ($request->getMethod() === 'GET') {
                return $this->responseFactory
                    ->createResponse(301)
                    ->withHeader('Location', (string) $uri);
            }

            // For non-GET requests, rewrite silently (no redirect)
            return $handler->handle($request->withUri($uri));
        }

        return $handler->handle($request);
    }
}

Register it early, before RoutingMiddleware:

$app->add(TrailingSlashMiddleware::class);
$app->addRoutingMiddleware();

@coveralls

coveralls commented Jun 13, 2026

Copy link
Copy Markdown

Coverage Status

coverage: 96.406% (-0.03%) from 96.432% — odan:v5-strict-routing into slimphp:5.x

@odan odan merged commit 7a9ca5a into slimphp:5.x Jun 13, 2026
4 checks passed
@odan odan deleted the v5-strict-routing branch June 13, 2026 21:06
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants