February 7, 2018
Angular Universal Handle 404 and Set Status Codes
When working with Angular Universal we can implement a full-fledged server. Of course, this does not only include for example to display a 404 page for incorrect urls but also setting the correct status code. So that for example search engines and crawlers in general can process the result correctly.
Angular itself allows us to easily create a 404 page via the router package. However, this is to be regarded as a soft 404. Because although a page is output, the HTTP status code is still 200.
In the first step we start with the trivial issue of displaying the Not-Found-Component. Afterwards we dedicate ourselves to setting the correct status code on the response object that we consume via Express. The example code assumes that you are using lazy load but you can change that.
Create a new module for our Not-Found-Component.
1 2 |
ng generate module not-found --routing ng generate component not-found |
Add the component to the not-found-routing.module.ts
file
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
import {NgModule} from '@angular/core'; import {RouterModule, Routes} from '@angular/router'; import {NotFoundComponent} from './not-found.component'; const routes: Routes = [{ path: '', component: NotFoundComponent }]; @NgModule({ imports: [RouterModule.forChild(routes)], exports: [RouterModule] }) export class NotFoundRoutingModule { } |
Add some content to the not-found.component.html file
1 2 3 4 |
<div class="text-center"> <span class="error-code">404</span> <h1>This page doesn't exist</h1> </div> |
Now only the module remains to be integrated into the routing by editing our app.routes.ts file. Add the following two entries to the end of your routes array. Watch out: redirectTo has to be last!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
import {NgModule} from '@angular/core'; import {RouterModule, Routes} from '@angular/router'; const routes: Routes = [ // .. routes { path: 'not-found', loadChildren: './routes/not-found/not-found.module#NotFoundModule' }, { path: '**', redirectTo: 'not-found' } ]; @NgModule({ imports: [RouterModule.forRoot(routes, { initialNavigation: 'enabled', }), ], exports: [RouterModule] }) export class AppRoutingModule { } |
So far so good. We now have a working 404 component that will be displayed when an invalid URL is accessed.
Setting the Status Code
The end users experience is covered. So lets set the correct status code. This tutorial assumes that you have followed the official Angular Universal Guide but should work for all setups using express. In our express server we have access to the request and response objects. Those are needed in the 404 component in order to set the status code each time the component is loaded. In order to do so we use the dependency injection provided by Angular.
Find the place where renderModuleFactory is used. Usually this is in the server.ts file in your projects root. It might look like this:
1 2 3 4 5 6 7 8 9 |
app.engine('html', (_, options, callback) => { renderModuleFactory(AppServerModuleNgFactory, { // Our index.html document: template, url: options.req.url, }).then(html => { callback(null, html); }); }); |
We are going to add some extraProviders
in order to inject response and request into our Angular application. Modify your code like this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
import {REQUEST, RESPONSE} from '@nguniversal/express-engine/tokens'; import {ValueProvider} from '@angular/core'; app.engine('html', (_, options, callback) => { renderModuleFactory(AppServerModuleNgFactory, { // Our index.html document: template, url: options.req.url, extraProviders: [ // make req and response accessible when angular app runs on server <ValueProvider>{ provide: REQUEST, useValue: options.req }, <ValueProvider>{ provide: RESPONSE, useValue: options.req.res, }, ] }).then(html => { callback(null, html); }); }); |
If you are not using @nguniversal/express-engine then simply use strings as injection tokens. When your application is now running on the server you have access to the request and response obejcts. If the application is running in the browser they are empty.
Now we can extend our Not-Found-Component in order to set the correct status code.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
import {Component, Inject, OnInit, Optional, PLATFORM_ID} from '@angular/core'; import {isPlatformBrowser} from '@angular/common'; import {RESPONSE} from '@nguniversal/express-engine/tokens'; import {Response} from 'express'; @Component({ selector: 'app-not-found', templateUrl: './not-found.component.html', styleUrls: ['./not-found.component.scss'] }) export class NotFoundComponent implements OnInit { constructor(@Inject(PLATFORM_ID) private platformId: Object, @Optional() @Inject(RESPONSE) private response: Response) { } ngOnInit() { if (!isPlatformBrowser(this.platformId)) { this.response.status(404); } } } |
Thats it. A hard page reload that leads to your Not-Found-Component will now result in the correct 404 error code. Feel free to leave a comment on how you liked the approach and if it worked for you!
Works like a charm..awesome ! Thanks a ton. I have been trying to solve this for a while now. Finally found this.
BTW…Though it worked perfectly during a server side build, I got the following error with the webpack compilation:
Critical dependency: the request of a dependency is an expression
Had to change some bit of the code in the not-found component. For some reason, webpack does not like injecting using RESPONSE token. Injected “Injector” instead and obtained the response using the following line:
this.injector.get(RESPONSE) as Response;
Thanks for your feedback! With the most recent version of Angular & CLI it is working for me. Instead of using the response as inejction token you can always create a string, then don’t forget to export it.
Same here, injector resolved the issue. Thanks 🙂
constructor(
@Inject(PLATFORM_ID) private platformId: Object,
@Inject(Injector) private injector: Injector
) {}
ngOnInit() {
if (!isPlatformBrowser(this.platformId)) {
let response = this.injector.get(RESPONSE) as Response;
response.status(404);
}
}
I’m also receiving “Critical dependency: the request of a dependency is an expression”. Can you please provide an example of creating a string and exporting this?
Also tried updating my CLI and Angular and still got the error.
can you please tell us what changes you made
After adding the value provider into the app engine, I’ve got an error: Error: StaticInjectorError[NgModuleFactoryLoader -> InjectionToken MODULE_MAP]:
StaticInjectorError(Platform: core)[NgModuleFactoryLoader -> InjectionToken MODULE_MAP]:
NullInjectorError: No provider for InjectionToken MODULE_MAP!
Hello Valentin,
your error isn’t really connected to the code from this blog post. Did you maybe replace the provider recipe for MODULE_MAP?
You can find an example usage here:
https://github.com/angular/angular-cli/wiki/stories-universal-rendering
Good Luck!
Thanks a lot! Excellent explanation!
[…] i’m trying to set 404 status for my wildcard route using angular universal ssr, please suggest me the way to do it. i already tried solution from [404 status code][1] https://blog.thecodecampus.de/angular-universal-handle-404-set-status-codes/ […]
Does this same code work for angular 6? If ‘No’, then what all do I need to change? If ‘Yes’ then mine is not working(Followed this procedure twice)!
I also have a problenm with angular 6. 404 not working.
now working in angular 6
Hi, Going to …/not-found does show the 404 page, but going to …/test just shows the google not-found page. I changed
loadChildren: './not-found/not-found.module#NotFoundModule'
because generating the not-found folder did not put it inside a routes folder. I don’t know if this helps, but I am using renderModuleFactory. – FloReally helpful.
Thanks a lot.
What if we need to send 404 for invalid js files, say for example main.8766576.js being accessed from browser. In a normal angular application this returns 200 status code with default index.html page. Is there a way to override this behaviour?
Since an invalid js file returns index.html the CDN actually caches the html hence causing problem for some users.
Toller Artikel, danke für die Weitergabe nützlicher Inhalte. Mit freundlichen Grüßen, David