8000 [Router] Optionnaly disable the trailing slash redirection behavior · Issue #33342 · symfony/symfony · GitHub
[go: up one dir, main page]

Skip to content

[Router] Optionnaly disable the trailing slash redirection behavior #33342

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
xavierleune opened this issue Aug 26, 2019 · 35 comments
Closed

[Router] Optionnaly disable the trailing slash redirection behavior #33342

xavierleune opened this issue Aug 26, 2019 · 35 comments
Labels

Comments

@xavierleune
Copy link
Contributor
xavierleune commented Aug 26, 2019

Hi,

Since SF 3.4 4.1, a new behavior regarding trailing slashes has been implemented. An other version of this behavior has been around forever. This behavior, largely documented, can be a little bit confusing and lead to some strange redirect in large applications.

As there is no real standard about this kind of redirect, I think this could be a better option to add a configuration key, to optionally disable all the automatic redirect to trailing slash or no stuff.

Following the comments on #32996 and #33362 this can be a valid use case to avoid a SEO risk.

Multiple options can be imagined to disable this behavior, with some pros and cons:

Global behavior:

General considerations:

Pros Cons
Easy to use, easy to understand If some 3rd party bundle relies on this feature, it can break things (suggested by @nicolas-grekas here - point 2 but not sure about a real use case here)

Use UrlMatcher instead of RedirectableUrlMatcher:

Pros Cons
No code, ready to use Disable also https/http redirections and 405

Add a configuration key:

Pros Cons
Easy to understand and to document
Very predictable

Local behavior:

General considerations:

Pros Cons
Greater control about this behavior Easy to forget
Unpredictable outcome if 2 concurrent routes have different configurations

Use and document the syntax described here (regex with negative lookahead to forbid final slash if unwanted):

Pros Cons
No code in sf Hard to read and to debug
Ready to use for dev

Option on route declaration

Pros Cons
Easy to understand and to use

What do you think ? I'll update the list if I think to more pros / cons

Thanks,

--
Edited on 2019-08-30 with more precise description

@xabbuh xabbuh added the Routing label Aug 26, 2019
@xavierleune
Copy link
Contributor Author

Closing in favor of #32996

@xavierleune
Copy link
Contributor Author

@Jean-ita do you have an opinion on this ?

@nicolas-grekas
Copy link
Member

Use UrlMatcher instead of RedirectableUrlMatcher:

did you give this a try? Maybe that's something that should be documented better and done?

@stof
Copy link
Member
stof commented Sep 9, 2019

Well, that also disable some other features

@xavierleune
Copy link
Contributor Author

@nicolas-grekas no I didn't, as @stof said, that also disable other features & I don't think that's a good alternative.

@boldtrn
Copy link
boldtrn commented Dec 8, 2019

Just to add another point in favor of disabling the trailing slash redirection behavior:

The automatic redirect can break urls if they use query parameter arrays - for example: example.com/path/?foo=bar1&foo=bar2 will be redirected to example.com/path/?foo=bar1. In earlier version of Symfony I could create my own redirection code that handles these issues, now I can't. So an easy option to keep BC here would be great 👍.

@perk11
Copy link
perk11 commented Sep 30, 2020

The documentation explains this as "nowadays it’s common to treat both URLs as the same URL and redirect between them".

Yes, it's common, but not universal. The URLs are exposed to the end user. This is something where the framework shouldn't get in the way. If I want to have an application that doesn't follow this, Symfony is not providing a proper way to do this without losing some other features.

I think adding a setting that would be checked in addition to whether RedirectableUrlMatcherInterface is implemented here https://github.com/symfony/routing/blob/7485aa5c8999dff509d1b433339a6b131dcdf845/Matcher/UrlMatcher.php#L135 would be a good way to resolve this for people that get struck by this behavior and have no other choice but use an UrlMatcher that doesn't implement RedirectableUrlMatcherInterface and lose some features.

@s3bul
Copy link
s3bul commented Oct 21, 2020

In my opinion url with / must be distinguished.
I have a situation in my router config

event_controllers:
  resource: ../../src/Module/Event/Controller/Api
  type: annotation
  name_prefix: event_
  prefix: /api/v1/event

and my controller looks like this

namespace App\Module\Event\Controller\Api;

(...)

/**
 * Class EventController
 *
 * @package App\Module\Event\Controller\Api
 *
 * @Rest\Route("", name="event_")
 */
class EventController extends ApiController
{
    /**
     * @return View
     *
     * @Rest\Get("", name="index")
     */
    public function index(): View

and in my swagger (i using nelmio/api-doc-bundle:^3.4) i have
Zrzut ekranu z 2020-10-21 10-35-58
it doesn't make sense...

@ivanbogomoloff
Copy link

Hello all.

This feature will be useful.
In my case i need url /anything/ - this url i want handle by controller, and sent 200 or 30x or 404 etc, and i have /anything - that must be 404
I solve this by 2 routes with ordering defintion.

//ordering of methods is important, if notFoundSlug will be first then it not working 
/**
     * @Route(path="/{slug}{slash}", name="hack_trailing_slash_200_ok",  requirements={"slash"="\/"})
     */
    public function foundBySlug()
    {
        return new Response("ok");
    }

    /**
     *  @Route(path="/{slug}", name="hack_trailing_slash_404_redirect")
     */
    public function notFoundSlug()
    {
        throw $this->createNotFoundException();
    }

But if i remove notFoundSlug method, url /anything/ and url /anything - always be 200 OK - this is incorrect behavior becase /anything != /anything/

@nicolas-grekas
Copy link
Member
nicolas-grekas commented Nov 16, 2020

@ivanbogomoloff you can match a route with a forced trailing / with this definition: /{slug}{_</(?!/)>}

@ivanbogomoloff
Copy link

@ivanbogomoloff you can match a route with a forced trailing / with this definition: /{slug}{_</(?!/)>}

Not working.
I try this early and now try and confirm that it not work

/**
     * @Route(path="/{slug}{_</(?!/)>}", name="slug_slash")
     */
    public function foundBySlug()
    {
        return new Response("ok");
    }

When i try /anything - this will redirect me to /anything/ and give me 200 ok.

I found another solution, but it scary too

/**
     * @Route(path="/{slug}{slash}", name="slug_slash", requirements={"slash" = ".+"})
     */
    public function foundBySlug(Request $request)
    {
        $url = $request->getPathInfo();
        if(!preg_match('/\/$/i', $url)) {
            // /anything will give 404
            throw $this->createNotFoundException();
        }
        // /anything/ give ok
        return new Response("ok");
    }

@nicolas-grekas
Copy link
Member
nicolas-grekas commented Nov 16, 2020

When i try /anything - this will redirect me to /anything/ and give me 200 ok.

Yes, that's because you'd also need to define (after the one with a slash) /anything and map it to a 404. Otherwise, the redirection logic does it magic yes.

I found another solution, but it scary too

It's how catch-all controllers are supposed to work yes.

@carsonbot
Copy link

Thank you for this issue.
There has not been a lot of activity here for a while. Has this been resolved?

@carsonbot
Copy link

Could I get an answer? If I do not hear anything I will assume this issue is resolved or abandoned. Please get back to me <3

@carsonbot
Copy link

Just a quick reminder to make a comment on this. If I don't hear anything I'll close this.

@carsonbot
Copy link

Could I get an answer? If I do not hear anything I will assume this issue is resolved or abandoned. Please get back to me <3

@carsonbot
Copy link

Friendly ping? Should this still be open? I will close if I don't hear anything.

@perk11
Copy link
perk11 commented Jun 1, 2021

@carsonbot As far as I'm aware this was not implemented yet.

@carsonbot carsonbot removed the Stalled label Jun 1, 2021
@carsonbot
Copy link

Thank you for this issue.
There has not been a lot of activity here for a while. Has this been resolved?

@ivanbogomoloff
Copy link

@carsonbot I could send fix to routing component, but i guess it might be incomptatable with "symfony way", but it works.
So, I tell about fix here and what i did.

  1. Add "special" parameter to "defaults" option in route description to all routes which might be without slash. Special parameter name is _trailing_slash and it must be boolean type.
  2. Check "_trailing_slash" parameter in Symfony\Component\Routing\Matcher\Dumper\CompiledUrlMatcherTrait at line ~106

Example:

class TrailingController extends AbstractController
{
    /**
     * @Route("/anything/", name="a2", defaults={"_trailing_slash": false})
     */
    public function anythingWitSlash()
    {
        // vendor/symfony/routing/Matcher/Dumper/CompiledUrlMatcherTrait.php
        return new Response($this->generateUrl('a1'));
    }


    /**
     * @Route("/anything", name="a1",  defaults={"_trailing_slash": false})
     */
    public function anythingWithoutSlash()
    {
        return new Response($this->generateUrl('a2'));
    }
}

Then in Symfony\Component\Routing\Matcher\Dumper\CompiledUrlMatcherTrait

//
foreach ($this->staticRoutes[$trimmedPathinfo] ?? [] as [$ret, $requiredHost, $requiredMethods, $requiredSchemes, $hasTrailingSlash, , $condition]) {
           //...a lot of code
            if ('/' !== $pathinfo && $hasTrailingSlash === ($trimmedPathinfo === $pathinfo)) {
                if ($supportsRedirections && (!$requiredMethods || isset($requiredMethods['GET']))) {
                    // Fix here at line ~106
                    if(isset($ret['_trailing_slash']) && $ret['_trailing_slash'] === false) {
                        continue;
                    }
                    return $allow = $allowSchemes = [];
                }
                continue;
            }
           //...a lot of code

In my case it works fine with other many routes.
So if it "right" fix, it can be documented at well in https://symfony.com/doc/current/routing.html#redirecting-urls-with-trailing-slashes , and will look similar "workaround" like trailing_slash_on_root = false

@carsonbot carsonbot removed the Stalled label Dec 6, 2021
@jontjs
Copy link
Contributor
jontjs commented Dec 17, 2021

Firstly, many thanks to @nicolas-grekas for your input into this problem, especially the suggestion to use

  • @Route("/hello{_</(?!/)>}", name="hello1") for the trailing-slash URL
  • @Route("/hello{_<(?!/)>}", name="hello2") for the non-trailing-slash URL

to allow both the trailing-slash and non-trailing-slash URL variants to exist, which I found in #32996.

However the router, when generating a URL for the trailing-slash URL, requires you to pass the _ parameter with a value of /, which is a little inconvenient (and easy to forget!). I've tried specifying a default parameter value (e.g. @Route("/hello{_<!/(?!/)>?/}", name="hello1")) but the route generator omits the trailing slash, and thus builds a URL which matches the non-trailing-slash URL, and therefore doesn't work (the final slug is a subcategory slug, not a product slug).

If you feel this belongs in a separate issue to hhis, please let me know. I think it's a relevent detail to the whole discussion around automatic redirection with/without trailings slashes.

As a little background, my use-case is that we have a requirement to add a "subcategory" layer to our URL hierarchy, and therefore change our URLs from

/{category}/
/{category}/{product}

to

/{category}/
/{category}/{subcategory}/
/{category}/{subcategory}/{product}

So we also need to retain /{category}/{product} so that it can redirect visitors (and search engine crawlers) to the new "product" pages.

I agree with what @perk11 said earlier in this issue:

The documentation explains this as "nowadays it’s common to treat both URLs as the same URL and redirect between them".

Yes, it's common, but not universal. The URLs are exposed to the end user. This is something where the framework shouldn't get in the way. If I want to have an application that doesn't follow this, Symfony is not providing a proper way to do this without losing some other features.

My feeling is that having the option to disable the automatic-redirection behaviour would save needing the "hacky" dummy route parameter.

@nelson6e65
Copy link

Or an option to aliasing instead of redirect.

In an API requests, the POST /api/products/ it fails due to redirection, instead of consume the existing /api/products endpoint directly.

@carsonbot
Copy link

Thank you for this issue.
There has not been a lot of activity here for a while. Has this been resolved?

@carsonbot
Copy link

Friendly reminder that this issue exists. If I don't hear anything I'll close this.

@BenjaminGoetz
Copy link

Symfony 4.4 user here, have this very problem, totally agree with @perk11 that while this redirection is common, it's clearly not universal.

@carsonbot carsonbot removed the Stalled label Jul 21, 2022
@BenjaminGoetz
Copy link

Oh, and by the way the « Route Matching Logs » of the profiler uses the full path to match, so its matching is wrong in such cases (it matches a route expecting a trailing slash where, in fact, it matched a non-trailing-slash-ending route earlier).

@carsonbot
Copy link

Thank you for this issue.
There has not been a lot of activity here for a while. Has this been resolved?

@carsonbot
Copy link

Hello? This issue is about to be closed if nobody replies.

@jontjs
Copy link
Contributor
jontjs commented Mar 16, 2023

I still agree with what @perk11 said earlier in this issue:

The documentation explains this as "nowadays it’s common to treat both URLs as the same URL and redirect between them".

Yes, it's common, but not universal. The URLs are exposed to the end user. This is something where the framework shouldn't get in the way. If I want to have an application that doesn't follow this, Symfony is not providing a proper way to do this without losing some other features.

An option to disable this particular "magic" behaviour would be welcome.

@carsonbot carsonbot removed the Stalled label Mar 16, 2023
@ssidorchik-zz
Copy link

is there still no option to turn off this behavior?

@carsonbot
Copy link
F438

Thank you for this issue.
There has not been a lot of activity here for a while. Has this been resolved?

@perk11
Copy link
perk11 commented Jan 28, 2024

@carsonbot As far as I'm aware this was not implemented yet.

@carsonbot carsonbot removed the Stalled label Jan 28, 2024
@carsonbot
Copy link

Thank you for this issue.
There has not been a lot of activity here for a while. Has this been resolved?

@carsonbot
Copy link

Could I get a reply or should I close this?

@Jean-Gian
Copy link

It can be close in my opinion

@carsonbot carsonbot removed the Stalled label Aug 19, 2024
@fabpot fabpot closed this as completed Aug 19, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

0