AngularJS and Handling 404 Errors
Asked Answered
E

5

45

What is the best way to serve up proper 404's with an AngularJS app?

A little background: I'm building an Angular app and have opted to use

$locationProvider.html5Mode(true);

because I want the URLs to appear natural (and indistinguishable from a multi-page "traditional" web app).

On the server side (a simple Python Flask app), I have a catch-all handler that redirects everything to the angular app:

@app.route('/', defaults={'path': ''})
@app.route('/<path>')
def index(path):
    return make_response(open('Ang/templates/index.html').read())

Now, I'm trying to figure out what to do with 404 errors. Most of the Angular apps I've seen do the following:

.otherwise({ redirectTo: '/' })

which means that there is no way they can serve up a proper 404.

However, I would much rather return a proper 404, with a 404 status code (mainly for SEO purposes).

What is the best way to handle 404s with Angular? Should I not worry about it and stick with a catch-all? Or should I remove the catch-all and serve up proper 404's on the server side?

edited for clarity

Exonerate answered 8/7, 2013 at 4:44 Comment(3)
what do you want to do on 404 could you mention your use caseDimitrovo
When one visits /asdf, one should be taken to a 404 page: "sorry, the page you are trying to access does not exist". A 404 status code should be returned.Exonerate
you could register a response interceptor and check the status code in the interceptor it you find 404 in http status code you could redirect to any path using $location.path('path')Dimitrovo
E
18

After playing around for a bit, as well as some discussions with Miguel, I compiled a few different solutions:

  1. Just use a catch-all and don't worry about proper 404's. This can be set up with server-side code (like in my original solution), or better, with URL re-writing on your web server.
  2. Reserve a certain section of your site for your angular app (like /app). For this section, set up a catch-all and don't worry about proper 404's. Your other pages will be served up as regular pages and visiting any invalid URL that doesn't start with /app will result in a proper 404.
  3. Continuously make sure that all of your routes in app.js are mirrored in your server-side code (yes, pretty annoying), where you'll have those routes serve up your angular app. All other routes will 404.

P.S. The second option is my personal favorite. I've tried this out and it works quite well.

Exonerate answered 9/8, 2013 at 18:31 Comment(3)
I'm running into a similar issue with an angular / node.js stack. I have a route that uses a specific ID for data lookup. For a good URL this works fine. For a bad URL I just wind up with a broken looking webpage. The urls looks like: /details/goodID vs. details/badID (I've also noticed certain bots like to try /details/details/goodID which is also a bad route :/). Solution #2 doesn't really handle this case, and I'd like to avoid #3 if at all possible. Did you run across any other ideas? Is there a 404-ish way to report the error client side??Garnishment
@Ryan - concerning #3, requiring a routes module with all the definitions via browserify isn't that bad right? That's how I've been doing it anyway.Homogenous
@Ryan Forgive me but doesnt this server call negate the whole point of Client SPA's? We call an SPA once and use Api's via Ajax from there. If you have to go to your server for each endpoint then why not simply use client/server model?Excitement
L
34

I think you are confusing Flask routes with Angular routes.

The 404 error code is part of the HTTP protocol. A web server uses it as a response to a client when the requested URL is not known by the server. Because you put a catch-all in your Flask server you will never get a 404, Flask will invoke your view function for any URLs that you type in the address bar. In my opinion you should not have a catch-all and just let Flask respond with 404 when the user types an invalid URL in the address bar, there is nothing wrong with that. Flask even allows you to send a custom page when a 404 code is returned, so you can make the error page look like the rest of your site.

On the Angular side, there is really no HTTP transaction because all the routing internal to the application happens in the client without the server even knowing. This may be part of your confusion, Angular links are handled entirely in the client without any requests made to the server even in html5mode, so there is no concept of a 404 error in this context, simply because there is no server involvement. An Angular link that sends you to an unknown route will just fall into the otherwise clause. The proper thing to do here is to either show an error message (if the user needs to know about this condition and can do something about it) or just ignore the unknown route, as the redirectTo: '/' does.

This does not seem to be your case, but if in addition to serving the Angular application your server implemented an API that Angular can use while it runs, then Angular could get a 404 from Flask if it made an asynchronous request to this API using an invalid URL. But you would not want to show a 404 error page to the user if that happened, since the request was internal to the application and not triggered by the user directly.

I hope this helps clarify the situation!

Litre answered 8/7, 2013 at 7:53 Comment(13)
I will remove the catch-all, because I want to be able to serve proper 404 errors (for SEO, etc.). However, now I have to keep a list of routes for Flask to pass on to Angular. That means that whenever I add a new route, I need to add it to both my app.js and my controllers.py. But honestly, I'd just like a single point of truth. How could I update the routes in one location and have them auto-reflected in both the JS and python code? I was thinking of creating a JSON file with the routes (the python would import the routes and a new app.js would be generated when the JSON file is touched).Exonerate
You are still confusing the server and the client side. Your Flask application has only one route, the "/" that serves the Angular application to the client. That's it. The Angular application can define as many routes as it needs, but those are client side, the Flask server is not involved in those. In fact, with such a simple setup it is unclear to me why you need Flask on the server, it seems you are just serving the static files for your Angular application.Litre
@Miguel I am not confusing them. Consider this: I have my angular application with "/" and "/about" in the angular routes. I also have html5Mode enabled. That means that if the user goes to "/", a request will be sent to the server and then the angular app will launch. When the user navigates to "/about", the request will not go to the server, since the angular app is now currently handling it. However, if the user refreshes the page, "/about" will be sent to the server. That means, there needs to be a "/about" endpoint on the server that passes the request onto angular.Exonerate
Sorry, yes, if you want the user to access the application from any of the links that show up in the address bar then you have to configure the server to rewrite all those URLs to "/" so that the main application is always served. Just found this in the Angular documentation regarding html5 mode: "Using this mode requires URL rewriting on server side, basically you have to rewrite all your links to entry point of your application (e.g. index.html)"Litre
Exactly. And this can be pretty annoying, since you may find yourself changing a URL in your Angular code but forget to do so in the server code. It is also not very elegant. Here's what I ended up doing: (1) put the routing info in a JSON file (2) set up my server to extract the endpoints in the routes and have those endpoints return "index.html" (for angular to handle) (3) wrote a script to re-generate a new app.js from the JSON file every time the server starts up. -- However, IMO, this is also pretty inelegant and I'm quite unsatisfied. Wish there was a better way.Exonerate
Or you can just follow the suggestion given by the angular guys and implement url rewriting, which seems easier and cleaner.Litre
That's what I had originally. So "/" and "/about" would both return "index.html". However, like I said before, you still have to continuously update the endpoints in two places.Exonerate
URL rewriting is something you configure on your web server, not on Flask. You can have Apache, nginx, etc. modify the incoming URLs so that your Flask server only ever sees "/". You are not planning to deploy your site on the Flask web server, right? That is not a production server.Litre
Yes I realize the Flask web server isn't a production web server, so I use Gunicorn and deploy to Heroku. Any idea how to do this with Heroku? What is the problem with having the Flask server return the single template for the Angular app? Latency?Exonerate
gunicorn is a more basic server, I believe you could implement rewriting using the pre_request hook, but I have never tried, not sure if that works. I think we've come full circle, if you had to implement this in Flask (and it seems that is the only reasonable choice), then the catch-all route that you started with is the best way, because you do not want to have the client side routes duplicated on the server. You will never get a 404 from Flask then, and if a client side route is invalid you can just redirect back to "/" from angular, or show an error if you prefer.Litre
I actually don't like the catch-all idea, since as you said, a 404 will never be served. What I ended up doing was have Flask redirect to the Angular application only if a given route existed. Otherwise, it would serve a 404. I designed my routes in a way that I have to do minimal updating on the server side - my CRUD-style routes redirect to angular only if a given URL endpoint corresponds to a defined model (and if an ID is specified, corresponds to an existing object in the db). Other routes like "/about" I just include individually.Exonerate
A conundrum! If you can execute JS on the server side, you could deploy your routes js file and actually invoke them on the server to decide whether to respond with HTTP 404... then you wouldn't have to update it in two places ... but you would have to deploy it to the server whenever you add routesHinman
I'd like to kindly point out that the answer above is an incorrect one, and wrongly indicates confusion on my part that simply was not there. It'd be nice to have this reflected in the beginning of the answer.Exonerate
E
18

After playing around for a bit, as well as some discussions with Miguel, I compiled a few different solutions:

  1. Just use a catch-all and don't worry about proper 404's. This can be set up with server-side code (like in my original solution), or better, with URL re-writing on your web server.
  2. Reserve a certain section of your site for your angular app (like /app). For this section, set up a catch-all and don't worry about proper 404's. Your other pages will be served up as regular pages and visiting any invalid URL that doesn't start with /app will result in a proper 404.
  3. Continuously make sure that all of your routes in app.js are mirrored in your server-side code (yes, pretty annoying), where you'll have those routes serve up your angular app. All other routes will 404.

P.S. The second option is my personal favorite. I've tried this out and it works quite well.

Exonerate answered 9/8, 2013 at 18:31 Comment(3)
I'm running into a similar issue with an angular / node.js stack. I have a route that uses a specific ID for data lookup. For a good URL this works fine. For a bad URL I just wind up with a broken looking webpage. The urls looks like: /details/goodID vs. details/badID (I've also noticed certain bots like to try /details/details/goodID which is also a bad route :/). Solution #2 doesn't really handle this case, and I'd like to avoid #3 if at all possible. Did you run across any other ideas? Is there a 404-ish way to report the error client side??Garnishment
@Ryan - concerning #3, requiring a routes module with all the definitions via browserify isn't that bad right? That's how I've been doing it anyway.Homogenous
@Ryan Forgive me but doesnt this server call negate the whole point of Client SPA's? We call an SPA once and use Api's via Ajax from there. If you have to go to your server for each endpoint then why not simply use client/server model?Excitement
K
2

This is an old thread, but I cam across it while searching for the answer.

Add this to the end of your appRoutes.js and make a 404.html view.

.when('/404', {
    templateUrl: 'views/404.html',
    controller: 'MainController'
})

.otherwise({ redirectTo: '/404' })
Keos answered 18/11, 2016 at 5:38 Comment(0)
G
0

I think that a real http 404 is going to be pretty useless "for SEO purposes" if you are not serving usable non-javascript content for real pages of your site. A search indexer is unlikely to be able to render your angular site for indexing.

If you are worried about SEO, you will need some sort of server side way to render the content that your angular pages are rendering. If you have that, adding 404s for invalid URLs is the easiest part of the problem.

Galosh answered 21/7, 2016 at 17:28 Comment(0)
A
-1

Here is the best way to handle the error and works nicely

function ($routeProvider, $locationProvider, $httpProvider) {
    var interceptor = [
        '$rootScope', '$q', function (scope, $q) {

            function success(response) {
                return response;
            }

            function error(response) {
                var status = response.status;

                if (status == 401) {
                    var deferred = $q.defer();
                    var req = {
                        config: response.config,
                        deferred: deferred
                    };
                    window.location = "/";
                }

                if (status == 404) {
                    var deferred = $q.defer();
                    var req = {
                        config: response.config,
                        deferred: deferred
                    };
                    window.location = "#/404";
                }
                // otherwise
                //return $q.reject(response);
                window.location = "#/500";
            }

            return function (promise) {
                return promise.then(success, error);
            };

        }
    ];
    $httpProvider.responseInterceptors.push(interceptor);
});

// routes
app.config(function($routeProvider, $locationProvider) {
    $routeProvider
        .when('/404', {
            templateUrl: '/app/html/inserts/error404.html',
            controller: 'RouteCtrl'
        })
        .when('/500', {
            templateUrl: '/app/html/inserts/error404.html',
            controller: 'RouteCtrl'
        })

        ......

   };
Atavistic answered 8/3, 2014 at 14:51 Comment(2)
I did not downvote - but I know SO prefers answers that aren't strictly code. Just a suggestion, as I'm grateful for most everyone who spends their time answering others' questions.Homogenous
This is the answer for the question "How do i intercept AND handle all 404 errors in angularjs". Im looking for this but couldn't find it, what i knew i was looking for was how to add an interceptor for all $http calls, as without this, angularjs will not treat a 404 as an error, just as another code and it will resolve in promises. I need a way to auto-error all 404's so i can catch them, for example in a login which returns a 404 when the login was unsuccessful. Hope it helps somebody #angularjs #httpinterceptor #interceptorPolymerous

© 2022 - 2024 — McMap. All rights reserved.