Skip to content
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

Trailing slash in url breaks state routing #50

Closed
jbellmore opened this issue Mar 15, 2013 · 63 comments
Closed

Trailing slash in url breaks state routing #50

jbellmore opened this issue Mar 15, 2013 · 63 comments
Assignees
Milestone

Comments

@jbellmore
Copy link

If the url contains a trailing slash the state routing does not recognize the url and transition to the correct state.

For example, if you define the following state:

$stateProvider
       .state('contacts', {
           url: '/contacts',
           templateUrl: '/contacts.html',
           controller: 'ContactsController'
       });

When you go to #/contacts it routes to the 'contacts' state correctly, however if you go to #/contacts/ the route is not recognized.

The only workaround I have found is to define a second route which contains the trailing slash with a different name but that is quite cumbersome to have to do for every single state in an application.

Thanks!

@cayuu
Copy link

cayuu commented Mar 19, 2013

Edit: mapping optional trailing slashes is default behaviour in Angular $routeProvider: angular/angular.js#784

Yes. This is also the default (and correct) behaviour of Angular's $routeProvider.

To understand why it's not the default behaviour to map URIs, consider that it's like asking the system to treat the resources url/index.html and url/index.html/ as the same thing. Which is usually you "doing it wrong". In some cases, you may want this, but at best it should be a flag (or perhaps an array of urls to map to the state). Or a rewrite rule you custom apply.

But it's worth noting that the existing behaviour is entirely correct.

@jeme
Copy link
Contributor

jeme commented Mar 19, 2013

@cayuu That is unfortunetly wrong.

Angular's $routeProvider does deal with this issue by creating redirection routes for trailing slashes.

Their when function where you can see it happen:

 this.when = function(path, route) {
    routes[path] = extend({reloadOnSearch: true, caseInsensitiveMatch: false}, route);

    // create redirection for trailing slashes
    if (path) {
      var redirectPath = (path[path.length-1] == '/')
          ? path.substr(0, path.length-1)
          : path +'/';

      routes[redirectPath] = {redirectTo: path};
    }

    return this;
  };

And a TestCase testing the behavior:

  it('should match route with and without trailing slash', function() {
    module(function($routeProvider){
      $routeProvider.when('/foo', {templateUrl: 'foo.html'});
      $routeProvider.when('/bar/', {templateUrl: 'bar.html'});
    });

    inject(function($route, $location, $rootScope) {
      $location.path('/foo');
      $rootScope.$digest();
      expect($location.path()).toBe('/foo');
      expect($route.current.templateUrl).toBe('foo.html');

      $location.path('/foo/');
      $rootScope.$digest();
      expect($location.path()).toBe('/foo');
      expect($route.current.templateUrl).toBe('foo.html');

      $location.path('/bar');
      $rootScope.$digest();
      expect($location.path()).toBe('/bar/');
      expect($route.current.templateUrl).toBe('bar.html');

      $location.path('/bar/');
      $rootScope.$digest();
      expect($location.path()).toBe('/bar/');
      expect($route.current.templateUrl).toBe('bar.html');
    });
  });

@cayuu
Copy link

cayuu commented Mar 19, 2013

Dude you are right about this. And thanks for the test code. My initial comment stemmed from an active project I was hacking on that is not doing this rewiring (because zombies and god knows what else) - but a simple demo shows it works in practice.

Edit: The zombies above happened to be this very project. If you add angular-ui-states.min.js and add the ui-compat injection, all default route re-rewriting stops working. Compare the first fidlle with the ui-router "broken" version

@ghost ghost assigned ksperling Mar 20, 2013
@ksperling
Copy link
Contributor

I was going to add support for this in the $route compat layer, but I'd like to hear some use cases for why this feature should be added to $state itself natively.

The only case where not redirecting to the correct version (whether that's with slash or without) could cause a problem is if you're expecting users to manually type in or edit URLs. Surely any links should be using the correct canonical version of the URL to begin with.

@ksperling
Copy link
Contributor

To be resolved as part of #17

@homerjam
Copy link

I ended up here looking for a way to deal with the trailing slash issue. I've configured all routes/states to match a trailing slash then I'm using this snippet to redirect if missing.

// Deal with missing trailing slash
$urlRouterProvider.rule(function($injector, $location) {
    var path = $location.path(), search = $location.search();
    if (path[path.length-1] !== '/') {
        if (search === {}) {
            return path + '/';
        } else {
            var params = [];
            angular.forEach(search, function(v, k){
                params.push(k + '=' + v);
            });
            return path + '/?' + params.join('&');
        }
    }
});  

@timkindberg
Copy link
Contributor

Wow that's great, would you mind if I added that to the FAQ?

@homerjam
Copy link

Of course, please do!

@oliviert
Copy link

Regarding that snippet, search === {} will always return false.

To check for an empty object, use this helper function:

function isEmpty(obj) {
  for (var key in obj) {
    return false;
  }
  return true;
}

@coolaj86
Copy link

Better snippet (a la @tomteman):

$urlRouterProvider.rule(function ($injector, $location) {
    var path = $location.url();

    // check to see if the path already has a slash where it should be
    if ('/' === path[path.length - 1] || path.indexOf('/?') > -1) {
        return;
    }

    if (path.indexOf('?') > -1) {
        return path.replace('?', '/?');
    }

    return path + '/';
});

Older snippet:

    $urlRouterProvider.rule(function($injector, $location) {
      var path = $location.path()
        // Note: misnomer. This returns a query object, not a search string
        , search = $location.search()
        , params
        ;

      // check to see if the path already ends in '/'
      if (path[path.length - 1] === '/') {
        return;
      }

      // If there was no search string / query params, return with a `/`
      if (Object.keys(search).length === 0) {
        return path + '/';
      }

      // Otherwise build the search string and return a `/?` prefix
      params = [];
      angular.forEach(search, function(v, k){
        params.push(k + '=' + v);
      });
      return path + '/?' + params.join('&');
    });

Then all routes in app/scripts/app.js must be redefined with trailing /. Note that routes such as /things/:id become /things/:id/ as well.

@timkindberg
Copy link
Contributor

@coolaj86 could you also update the FAQ for me?!

@coolaj86
Copy link

coolaj86 commented Dec 2, 2013

@timkindberg Done.

@timkindberg
Copy link
Contributor

Thanks man!

@steve-lorimer
Copy link

How does one declare the urls for nested routes when using this rule config?

    .state('parent', {
            url: '/parent/',
    }
    .state('parent.child', {
            url: '/:id/',
    }

Attempting to navigate to either /parent/1 or /parent/1/ doesn't work

@homerjam
Copy link

Hi Steve. You need to remove the first slash in the child state.
On 17 Feb 2014 08:47, "Steve Lorimer" [email protected] wrote:

How does one declare the urls for nested routes when using this ruleconfig?

.state('parent', {
        url: '/parent/',
}
.state('parent.child', {
        url: '/:id/',
}

Attempting to navigate to either /parent/1 or /parent/1/ doesn't work

Reply to this email directly or view it on GitHubhttps://github.com//issues/50#issuecomment-35234736
.

@steve-lorimer
Copy link

Thanks!

@mwtorkowski
Copy link

To me solution proposed in FAQ wasn't nice enough, as I didn't want every view to end with slash, I rather wanted to get rid of trailing slash if it has been added. Here's what I ended up with - much simpler, much nicer IMO:

$urlRouterProvider.rule(function($injector, $location) {
    var path = $location.path();
    if (path != '/' && path.slice(-1) === '/') {
        $location.replace().path(path.slice(0, -1));
    }
});

@homerjam
Copy link

homerjam commented Jun 3, 2014

Nice! It is preference of course 😉

This working ok with $location.search() params?

@mwtorkowski
Copy link

It sure does work as it doesn't even touch search part of the url, just replacing path.

@uberspeck
Copy link

@mwtorkowski, i like your solution...however, I'm not having any luck. My "otherwise" method keeps getting triggered. On login, my default page is @ #/manage/residents. So... If I load #/manage/residents it works as expected, however if manually change the path and reload or link to #/manage/residents/ I'm redirected to 404!? The only thing i can figure is my when() statements are overruling it somehow?

myApp
  .config( ($urlRouterProvider) ->

    $urlRouterProvider.rule( ($injector, $location) ->
      path = $location.path()
      if path isnt "/" and path.slice(-1) is "/"
        console.log( path.slice(0,-1) ) # returns path minus trailing "/" as expected!!!
        $location.replace().path( path.slice(0, -1) )
        return
    )

    $urlRouterProvider
      .when("","/")
      .when("/", "/manage/residents")
      .when("/manage", "/manage/residents")
      .when("/assess", "/assess/to_do")
      .otherwise("/errors/404")
  )

@tomteman
Copy link

Since $location.path() decodes the URL, the suggested solution caused problems for us when using special chars in the path.

We came up with a solution which we believe is much cleaner and less disruptive, by using $location.url(), which doesn't affect the path values (i.e - doesn't decode):

$urlRouterProvider.rule(function ($injector, $location) {
    var path = $location.url();

    // check to see if the path already has a slash where it should be
    if (path[path.length - 1] === '/' || path.indexOf('/?') > -1) {
        return;
    }

    if (path.indexOf('?') > -1) {
        return path.replace('?', '/?');
    }

    return path + '/';
});

@coolaj86
Copy link

@coolaj86
Copy link

This makes the inverse so much simpler as well:

$urlRouterProvider.rule(function ($injector, $location) {
    var path = $location.url();

    // check to see if the path has a trailing slash
    if ('/' === path[path.length - 1]) {
        return path.replace(/\/$/, '');
    }

    if (path.indexOf('/?') > -1) {
        return path.replace('/?', '?');
    }

    return path;
});

I didn't test it yet, but does that look right?

@tomteman
Copy link

Looks all right to me

@bleuscyther
Copy link

Perfect! @tomteman
the above solution fixed graciously the issue related to trailing spaces.

However I may be wrong but it removes the trailing space and this becomes wrong :

You'll need to specify all urls with a trailing slash if you use this method.
and
Note: All routes in app/scripts/app.js must be redefined with trailing /. This means that routes such as /things/:id become /things/:id/ as well.

From my test it only works if there is no / at the end.

I put the snippet rule before adding any $stateProvider.state().

Of course any .htaccess to add trailing space becomes useless

<IfModule mod_rewrite.c>
RewriteCond %{REQUEST_URI} /+[^\.]+$
RewriteRule ^(.+[^/])$ %{REQUEST_URI}/ [R=301,L]
</IfModule>

@homerjam
Copy link

I had to do the following when using @coolaj86 's 'no trailing slash' approach above (change return path to return false):

$urlRouterProvider.rule(function ($injector, $location) {
    var path = $location.url();

    // check to see if the path has a trailing slash
    if ('/' === path[path.length - 1]) {
        return path.replace(/\/$/, '');
    }

    if (path.indexOf('/?') > -1) {
        return path.replace('/?', '?');
    }

    return false;
});

@jamesbrobb
Copy link

What version are you using? Have you looked at this as another option?

$urlMatcherFactoryProvider.strictMode(false);

@Adriien-M
Copy link

Thanks for your response, I updated ui-router to the last version and it works. But I still have to add this option in each module.

Also, what strictMode exactly do?

saravmajestic added a commit to saravmajestic/angularjs-server that referenced this issue Jan 10, 2015
Fixing trailing slash issue - when navigating from home to any city (ex: boston) and reload the page, somehow '\' is added to the end of location.href, which breaks the routing.
This code will remove the trailing slash
Taken from angular-ui/ui-router#50 (comment)
@pasupulaphani
Copy link

I have had an issue with html5mode(true) and has an hash in a url it goes to infinite loop.

http%3A%2F%2Fexample.com%2Fposts%2F
http%25253A%25252F%25252Fexample.com%25252Fposts%25252F
http%252525253A%252525252F%252525252Fexample%252525252Fposts%252525252F

@13protons
Copy link

@homerjam your last code snippet in this thread worked great for me!

@rebelliard
Copy link

The snippet on the FAQ breaks when you html5mode(true) and try to use a hash on a URL, as pointed by @pasupulaphani here.

This version from @homerjam works, however.

@snaikaw
Copy link

snaikaw commented Oct 1, 2015

If you just want to allow "/" at the end of the URL, then you can use regular expression like this:

$stateProvider
       .state('contacts', {
           url: '/contacts{dummyParam:[/]?}',
           templateUrl: '/contacts.html',
           controller: 'ContactsController'
       });

@kbiesbrock
Copy link

Nice, @snaikaw!

@jgentes
Copy link

jgentes commented Oct 21, 2015

I found $urlMatcherFactoryProvider.strictMode(false) to be the simple solution I was looking for.

@pfrankov
Copy link

@jgentes you just saved my day! Thank you so much!

@jml6m
Copy link

jml6m commented Dec 18, 2015

Just want to say @jgentes method did not work for me

@jgentes
Copy link

jgentes commented Dec 18, 2015

@jml6m if you're using Node/Express, this is another approach to remove trailing slashes:

// remove trailing slash
app.use(function(req, res, next) {
    if(req.url.substr(-1) == '/' && req.url.length > 1)
        res.redirect(301, req.url.slice(0, -1));
    else
        next();
});

@jml6m
Copy link

jml6m commented Dec 18, 2015

I ended up using this method:

$urlRouterProvider.rule(function($injector, $location) {

    var path = $location.path();
    var hasTrailingSlash = path[path.length-1] === '/';

    if(hasTrailingSlash) {

      //if last charcter is a slash, return the same url without the slash  
      var newPath = path.substr(0, path.length - 1); 
      return newPath; 
    } 

  });

@stefanotorresi
Copy link

I've found @jml6m's method to be the simplest at getting rid of the trailing slash.

@gaui
Copy link

gaui commented Feb 10, 2016

I've found @jgentes solution to be the best.

@mleanos
Copy link

mleanos commented Feb 28, 2016

The solutions that add a $urlRouterProvider.rule work fine. However, if you define your UI routes in separate configurations (in their respective module) you are required to define this same logic in each of your module configurations. I wasn't too fond of that idea. If I'm wrong in my assessment of how these rules work, then please correct me. I had had no luck adding this to my application configuration. And from what I read, it's just not possible.

Since I didn't want to add this to each module's route configuration, I ended up adding a function that does pretty much what @jml6m's solution does to the beginning of the $stateChangeStart event.

  // Check authentication before changing state
  $rootScope.$on('$stateChangeStart', function (event, toState, toParams, fromState, fromParams) {
    // Ensure we don't transition to a URL with a trailing slash
    preventTransitionWithTrailingSlash(event);
  ...... // other logic that will execute if no trailing slash is found
  });

  function preventTransitionWithTrailingSlash(event) {
    var path = $location.$$url;

    // Check if the current transition has a trailing slash
    if (path.length > 1 && path[path.length - 1] === '/') {
      event.preventDefault();

      // $location.path() needs an async operation to work
      $timeout(function () {
        $location.path(path.substr(0, path.length - 1));
      });
    }
  }

Does anyone see any issues with this approach? I would love a cleaner solution. I find it odd that there isn't an easier way to handle this at an application level.

@stefanotorresi
Copy link

Hmm I'm not sure I understand why you can't just use the $urlRouterProvider.rule. You only have to do that once, there is no need to repeat it in each module.

Also, you can put the snippet in a dedicated module and have all other modules depend on it.

@mleanos
Copy link

mleanos commented Feb 28, 2016

@stefanotorresi You're absolutely right! I was misinterpreting the behavior I was seeing, as the Rule not getting applied to my sub-modules. There seems to be a couple things going on with the issues that I'm having.

  1. Nested routes can cause conflicts, when the URL is defined like this: { url: '/:parameter' }
For example, the below config will cause conflicts

articles      - { url: '/articles', abstract: true }
articles.list - { url: '' }
articles.view - { url: '/:articleId' }

Is the solution to refactor this route config? The below routes won't have the same conflict

articles      - { url: '/articles', abstract: true }
articles.list - { url: '' }

article       - { url: '/article', abstract: true }
article.view  - { url: '/:articleId' }
  1. The load order of the angular modules might be causing weird behavior with the $urlRouterProvider service.

I'm working with the MEANJS project, and I've pushed a branch up to my fork to demonstrate the behavior I'm experiencing. The diffs will show you where I'm having issue, and I've added some inline comments to further explain. I apologize if this is a bit much, but at this point, I could use some more eyes on this; been working on this for over 10 hours now.

mleanos/mean@132d23a

@esetnik
Copy link

esetnik commented Jun 2, 2016

I found the @jml6m solution didn't work because it seems that the rule is attempted only after a failure to match on an existing state. From @mleanos example above the articles.view is still triggered and the the rule is never applied if the user navigates to /articles/ 😦

@esetnik
Copy link

esetnik commented Jun 2, 2016

Oh nevermind... it turns out that there's a race condition between $stateProvider.state and $urlRouteProvider.rule. If you set the $urlRouteProvider.rule before the $stateProvider.state then it is applied on every request, but if you set it after then it is only applied on requests that fail to match any state configs. @mleanos maybe this is what you were experiencing above.

@javoire
Copy link

javoire commented Oct 26, 2016

@mleanos, your fix works for me, but I need to trigger it on $locationChangeStart for it to work fully, i.e. before ui router kicks in. Covering the case of when someone navigates directly (i.e. not clicking a link) to a url with trailing slash. Just wanted to mention that if others run into it. Cheers

@christopherthielen
Copy link
Contributor

@javoire ui-router offers deferIntercept for doing things before the URL is synchronized.

@javoire
Copy link

javoire commented Oct 27, 2016

Thanks for the info! I'll try it out
On Wed, 26 Oct 2016 at 18:51, Chris Thielen [email protected]
wrote:

@javoire https://github.com/javoire ui-router offers deferIntercept
https://ui-router.github.io/docs/latest/classes/url.urlrouterprovider.html#deferintercept
for doing things before the URL is synchronized.


You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub
#50 (comment),
or mute the thread
https://github.com/notifications/unsubscribe-auth/AAnPNVJCGzpXwgXhZkROcPgomwA7b-zbks5q34T-gaJpZM4AgQO3
.

@tsuz
Copy link

tsuz commented Jan 9, 2017

$urlMatcherFactoryProvider.strictMode(false); will match '/path' and '/path'/

@abhaygarg
Copy link

Hi Now ui-router have strictMode to handle it
You need to set strictMode = false
Ex
$urlMatcherFactoryProvider.strictMode(false);
You need to set strict mode before initialising the State $stateProvider.state({})

For more details you can refer this Link

@adamreisnz
Copy link

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

No branches or pull requests