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
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!
After playing around for a bit, as well as some discussions with Miguel, I compiled a few different solutions:
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.
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.
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.
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' })
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.
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'
})
......
};
Related
I already searched for a good while on the Internet and even checked all suggested questions here, but I found nothing.
Basically, I'm using vue-router to load views when the user clicks on them (without prefetching, just lazy-loading), using a function that imports the Vue view/component. To better visualize, I made a barebone example of what I'm doing:
const router = new VueRouter({
routes: [
...
{
path: "/a_page",
component: function() {
return import("./views/A_Page.vue");
}
}
]
});
I'm using Express in the backend to protect certain routes, because protecting it in the Frontend is wasted effort, since the user could bypass the 'protection' easily, if he wants to. Also all views have their own splitted .js file (using "webpackChunkName") and Express needs a Bearer Authentication Token header for every API call OR .js file requested. This works great with Axios (responsible for fetching API data) where you can manually define a header, but vue-router hasn't this option, and since it doesn't send the Authorization header, it doesn't authenticate, Express blocks the file with a 403 and vue-router fails to import the file.
Is there any way to send the Authorization header with the import (which is basically just a GET request)?
Thanks in advance.
If someone thinks I'm approaching the problem in a wrong way, feel free to comment and suggest another way.
EDIT: The suggested duplicate question was given too little attention and the only solution given (which is basically split in 2) doesn't work with the current webpack anymore; onload(event) and onerror(event) get undefined.
You could use a router guard instead of protecting with basic auth.
I use this method, along with lazy loaded routes. If the auth fails you can redirect the user to a login page.
const router = new VueRouter({ ... })
router.beforeEach((to, from, next) => {
If (auth) { // get value of cookie etc for a jwt token or similar auth method
next() // proceed
}
else {
next(false) // cancel navigation
// or
next(“/login-url”) // redirect you login url
}
})
Additionally, you could use an axios method to auth on each route change.
If you want to send up the Authorization header (which doesn't seem to be an easy task, given that no one knows how to go about it...) I think you could override webpack's jsonp function that it uses to load the chunks in splitChunks...
Here's the docs for the webpack function that loads the chunks
You'll override your webpack config with your modified chunk loading function and then tie that into your vue.config.js like so...
// vue.config.js
module.exports = {
configureWebpack: require('./webpack.config.js')
}
All this being said, I would suggest protecting your frontend assets much earlier than when you need to be loading your split chunks and not requiring the Authorization header to serve your static assets.
Sometimes you can do this at the network layer (load balancer, etc) depending on your use-case. Other times using a server-based approach, like rendering your app w/ Nuxt, will be what you want.
If I'm understanding correctly (feel free to correct me), would you be able to do an auth call with axios prior to the actual routing, or perhaps upon the routing using a mounted call? Especially if there is no valid authentication you can then either redirect to a login page or re-route to an error page.
Feel free to let me know if I'm misunderstanding.
Would like to check what is the issue caused to my <a href> links when after I introduced an interceptor to my angular app and it has cause the links to not reload when it is on the same page? below is how i introduce my interceptor to add jwt's Authentication token to my web service requests header.
app.config(['$httpProvider', function ($httpProvider) {
$httpProvider.interceptors.push(['$q', '$localStorage', '$location', function ($q, $localStorage, $location) {
return {
'request': function (config) {
config.headers = config.headers || {};
if ($localStorage.jwtToken) {
config.headers.Authorization = 'Bearer ' + $localStorage.jwtToken;
}
return config;
},
'responseError': function (response) {
if (response.status === 401 || response.status === 403) {
}
return $q.reject(response);
}
};
}]);
}]);
Noted that the presentation site and the business logic processing part are both independently separated and do not rely on each other. Which means that the presentation site is responsible to load the javascripts and HTML codes while the scripts are the one that is responsible to request data from the server. Authentication are done via JWT therefore I use the interceptor to inject the JWT related headers to every RESTful requests.
PHP => (renders HTML template) + (data from Angular) <= Angular => (send request to server get data)
Using the code above i was able to complete the JWT authentication but it causes all the <a href> links on the presentation page to not reload if it is in the same page. For example I have 3 items in my menu (Home, Page1, Page2). When I'm in Home and if I click on the Home link, it suppose to reload the page (like F5) but nothing happens. I would need to navigate away from the page then only i am able to click on the link.
What have i done wrong here?
Update 1: Question from #Sanjeev: How are you handling routing in you app, are you using ng-router module or custom ui-router module ? Can you add the routing code as well.
Noted that at this moment all routes are within the HTML itself using links. The javascripts do not handle any routes. Its responsibility is to GET and POST data.
Update 2: Added plunker link. Note that i would suggest you to try both commenting and uncommenting the entire interceptor section to see the difference when clicking the link. Follow these instruction below and you will recreate the scenario i mentioned.
Load and run the plunker file
On the top right corner, click on "Launch the preview in a separate window"
Copy the URL in the window and replace it in the section in line 25. The url should look something like run.plnkr.co/somerandomkeys+
Close the separate window and try clicking the link in the menubar.
When commenting the said section, notices that the page will load (acts as a refresh) but when you uncomment the section the link will not work anymore. Some sort of same page detection thing is blocking the action.
Solution :
Analysis: I ran your demo and understood the issue you were highlighting, actually the issue is not related to interceptors at all. Interceptors get called only when you make http requests using $http service.
In Angular apps the anchor tag behavior changes the moment you inject '$location' service in your app, you have injected '$location' service in your interceptor module (although i don't see it being used). So this solves the mystery why you start getting anchor issue when you add interceptor :)
In your example the anchor has same link as the current location so Angular is preventing the default behavior of anchor tag and clicking anchor does not reload your page.
You can solve it be multiple ways:
Don't inject '$location' service if you are not using it, if you can't remove it then go for solution 2 or 3.
Add attribute target="_self" or target="_blank" as per your case, this will solve your issue without requiring any Js code change. I tested this fix with your code and it worked for me.
Add a ng-click handler on anchors and change window.location in
it, or better create a directive for anchors and check if href is
same as current location then force page reload using location.reload()
If you decide to use angular routing which is great feature of Angular JS then use $route.reload() method
I have an application set up with the Mean.js yeoman generator. It uses PassportJS to setup local-authentication. This all works great and I understand how to check if a user is logged in from the ExpressJS routes file.
The problem is that most of my page routing is done in the angular routes. I know how to check authentication in the controller with the the following code.
// Projects controller
angular.module('projects').controller('ProjectsController', ['$scope', '$stateParams', '$location', 'Authentication', 'Projects',
function($scope, $stateParams, $location, Authentication, Projects) {
$scope.authentication = Authentication;
But how do I check authentication in the routes. For example in this routes files how would I only allow authenticated users to access the tools html file and redirect users that arent logged in back to the home page:
'use strict';
//Setting up route
angular.module('analysis').config(['$stateProvider',
function($stateProvider) {
// Projects state routing
$stateProvider.
state('imageAnalysis', {
url: '/analysis',
templateUrl: 'modules/analysis/views/tools.client.view.html'
});
}
]);
I know there are similar posts out there but I had trouble understanding many of them. Thanks, I really appreciate any help. I am new to the stackoverflow community and still learning community standards.
At a high level, there are two approaches:
Use your view routing layer (e.g. UI Router) to catch “unauthenticated” state
Use HTTP interceptors to look for requests that have a 401 Unauthorized status, indicating that the user must login (or that their current session has expired)
In practice you’ll probably use a combination of both.
Speaking to the UI Router, there a two of doing this: resolves or events.
Resolves: The UI Router provides a nice feature called resolves, which allows you to provide a promise that must be resolved before the view is rendered. You could create a promise which resolves your user state.
Events: The UI Router provides a $stateChangeStart event, you can observe this event and prevent it if the user is not logged in. You would then send the user to login page. This is a bit more stateful (you have to remember where the user wanted to go in the first place, so that you can redirect after login).
I chose the event approach for my work on the Stormpath Angular SDK because it gives me the flexibility to define authorization on top of authentication.
You may be looking for HTTP Interceptors. Check auth on the requests.
From OneHungryMind:
HTTP interceptors are a great way to define behavior in a single place
for how a request or response is handled for ALL calls using the $http
service. This is a game changer when you want to set an auth token on
all outgoing calls or respond to a particular HTTP status error at the
system level. You can pair interceptors with the incredibly useful
Angular Storage module to cache an authenticated user and retrieve it
the next run time.
There are some good tutorials(1) out there(2)!
I have a AngularJS app working with html5mode set to true.
Currently, the app shows a soft 404 page, with the .otherwise setting in the router.
Is there a way I could serve actual 404 HTTP response, for the sake of SEO while using html5mode?
If I understand correctly what you want, you have to do the following:
hard redirect the browser (bypassing the angular routing) on the otherwise path, with something like this:
$routeProvider
.otherwise({
controller: function () {
window.location.replace('/404'); // your custom 404 page
// or a non existing page
}
});
if you have a regular single-page-application where all the server request are redirected to the same SPA entry point, you have to configure on your server to make a routing exception for your custom 404 page, which will should also be served with a 404 status.
Otherwise, I can't see how you would do that with just History API, without an external request, because it's whole point in angular routing is to bypass external requests.
If you just want non-existing routes to return 404, then you must configure your server to match all your angular routes, and return 404 otherwise.
Seach engines works with SPA applications through prerendered pages, using _escaped_fragment_ . And you can use Prerender.io (or simply PhantomJS) to generate any status codes for search engines, like this https://prerender.io/documentation/best-practices
But this schema is deprecated by Google: http://googlewebmastercentral.blogspot.ru/2015/10/deprecating-our-ajax-crawling-scheme.html At this moment Google tries to understand your JS with usual crawling schema.
Hard redirection to 404.html page is not a good practice: url must stay the same, like https://stackoverflow.com/somepage
You can try Angular2 with server rendering feature: https://docs.google.com/document/d/1q6g9UlmEZDXgrkY88AJZ6MUrUxcnwhBGS0EXbVlYicY/edit
You have to make your server issue 404s. Angular cannot help in anyway here.
I currently have a set-up based on the meanjs stack boilerplate where I can have users logged in this state of being 'logged-in' stays as I navigate the URLs of the site. This is due to holding the user object in a Service which becomes globally available.
However this only works if I navigate from my base root, i.e. from '/' and by navigation only within my app.
If I manually enter a URL such as '/page1' it loses the global user object, however if I go to my root homepage and navigate to '/page1' via the site. Then it's fine, it sees the global user object in the Service object.
So I guess this happens due to the full page refresh which loses the global value where is navigating via the site does not do a refresh so you keep all your variables.
Some things to note:
I have enabled HTML5Mode, using prefix of '!'.
I use UI-Router
I use a tag with '/'
I have a re-write rule on express that after loading all my routes, I have one last route that takes all '/*' to and sends back the root index.html file, as that is where the angularjs stuff is.
I'm just wondering what people generally do here? Do they revert the standard cookies and local storage solutions? I'm fairly new to angular so I am guessing there are libraries out there for this.
I just would like to know what the recommended way to deal with this or what the majority do, just so I am aligned in the right way and angular way I suppose.
Update:
If I manually navigate to another URL on my site via the address bar, I lose my user state, however if I manually go back to my root via the address bar, my user state is seen again, so it is not simply about loosing state on window refresh. So it seems it is related to code running on root URL.
I have an express re-write that manually entered URLs (due to HTML5 Location Mode) should return the index.html first as it contains the AngularJs files and then the UI-Route takes over and routes it properly.
So I would have expected that any code on the root would have executed anyway, so it should be similar to navigating via the site or typing in the address bar. I must be missing something about Angular that has this effect.
Update 2
Right so more investigation lead me to this:
<script type="text/javascript">
var user = {{ user | json | safe }};
</script>
Which is a server side code for index.html, I guess this is not run when refreshing the page to a new page via a manual URL.
Using the hash bang mode, it works, which is because with hash bang mode, even I type a URL in the browser, it does not cause a refresh, where as using HTML5 Mode, it does refresh. So right now the solution I can think of is using sessionStorage.
Unless there better alternatives?
Update 3:
It seems the best way to handle this when using HTML5Mode is that you just have to have a re-write on the express server and few other things.
I think you have it right, but you may want to look at all the routes that your app may need and just consider some basic structure (api, user, session, partials etc). It just seems like one of those issues where it's as complicated as you want to let it become.
As far as the best practice you can follow the angular-fullstack-generator or the meanio project.
What you are doing looks closest to the mean.io mostly because they also use the ui-router, although they seem to have kept the hashbang and it looks like of more of an SEO friendly with some independant SPA page(s) capability.
You can probably install it and find the code before I explained it here so -
npm install -g meanio
mean init name
cd [name] && npm install
The angular-fullstack looks like this which is a good example of a more typical routing:
// Server API Routes
app.route('/api/awesomeThings')
.get(api.awesomeThings);
app.route('/api/users')
.post(users.create)
.put(users.changePassword);
app.route('/api/users/me')
.get(users.me);
app.route('/api/users/:id')
.get(users.show);
app.route('/api/session')
.post(session.login)
.delete(session.logout);
// All undefined api routes should return a 404
app.route('/api/*')
.get(function(req, res) {
res.send(404);
});
// All other routes to use Angular routing in app/scripts/app.js
app.route('/partials/*')
.get(index.partials);
app.route('/*')
.get( middleware.setUserCookie, index.index);
The partials are then found with some regex for simplicity and delivered without rendering like:
var path = require('path');
exports.partials = function(req, res) {
var stripped = req.url.split('.')[0];
var requestedView = path.join('./', stripped);
res.render(requestedView, function(err, html) {
if(err) {
console.log("Error rendering partial '" + requestedView + "'\n", err);
res.status(404);
res.send(404);
} else {
res.send(html);
}
});
};
And the index is rendered:
exports.index = function(req, res) {
res.render('index');
};
In the end I did have quite a bit of trouble but managed to get it to work by doing few things that can be broken down in to steps, which apply to those who are using HTML5Mode.
1) After enabling HTML5Mode in Angular, set a re-write on your server so that it sends back your index.html that contains the Angular src js files. Note, this re-write should be at the end after your static files and normal server routes (e.g. after your REST API routes).
2) Make sure that angular routes are not the same as your server routes. So if you have a front-end state /user/account, then do not have a server route /user/account otherwise it will not get called, change your server-side route to something like /api/v1/server/route.
3) For all anchor tags in your front-end that are meant to trigger a direct call to the server without having to go through Angular state/route, make sure you add a 'target=_self'.