45
votes

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

5
what do you want to do on 404 could you mention your use case - Ajay Beniwal
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. - Ryan Shea
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') - Ajay Beniwal

5 Answers

34
votes

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!

18
votes

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.

2
votes

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' })
0
votes

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.

-1
votes

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'
        })

        ......

   };