Search code examples
angularasp-net-core-spa-services

Return 404 status code in aspnet core SPA Angular application


I am working on a project using the Angular SPA project template for dotnet core (the one available in VS2017 and dotnet core 2.0).

I have an Angular component to display a "Not Found" message if a user goes to an invalid URL.

I also have server-side pre-rendering enabled. When returning the prerendered "Not Found" page from the server, how would I make it return an HTTP status code of 404?

Every method I have found to do this uses Express as the backend webserver, I have not been able to find any resources for doing this on an aspnet core backend.

Thanks for any help!


Edit for clarity:

I am not looking to return a 404 for a specific MVC controller action or on an application error.

I am looking to return a 404 from a specific Angular2/4 component, rendered server-side by Microsoft.AspNetCore.SpaServices.

For comparison, here is an example of a solution using the Express web server on NodeJS and angular universal for server-side rendering.


Solution

  • I have found a solution.

    First, we need to create an injectable service that components can use to set the status code:

    import { Injectable } from '@angular/core';
    
    @Injectable()
    export class HttpStatusCodeService {
    
      private statusCode: number;
    
      constructor(){
        this.statusCode = 200;
      }
    
      public setStatusCode(statusCode: number) {
        this.statusCode = statusCode;
      }
    
      public getStatusCode(): number {
        return this.statusCode;
      }
    }
    

    We will need to add this service to the providers array in the main AppModule module:

    ...
    providers: [
        ...
        HttpStatusCodeService,
        ...
    ]
    ...
    

    And then we need to add two lines (plus the import statement) within our boot.server.ts file (note this is based on the stock file created by the VS2017 template):

    import 'reflect-metadata';
    import 'zone.js';
    import 'rxjs/add/operator/first';
    import { APP_BASE_HREF } from '@angular/common';
    import { enableProdMode, ApplicationRef, NgZone, ValueProvider } from '@angular/core';
    import { platformDynamicServer, PlatformState, INITIAL_CONFIG } from '@angular/platform-server';
    import { createServerRenderer, RenderResult } from 'aspnet-prerendering';
    import { AppModule } from './app/app.module.server';
    
    //ADD THIS LINE
    import { HttpStatusCodeService } from './path/to/services/http-status-code.service';
    
    enableProdMode();
    
    export default createServerRenderer(params => {
        const providers = [
            { provide: INITIAL_CONFIG, useValue: { document: '<app></app>', url: params.url } },
            { provide: APP_BASE_HREF, useValue: params.baseUrl },
            { provide: 'BASE_URL', useValue: params.origin + params.baseUrl },
        ];
    
        return platformDynamicServer(providers).bootstrapModule(AppModule).then(moduleRef => {
            const appRef: ApplicationRef = moduleRef.injector.get(ApplicationRef);
            const state = moduleRef.injector.get(PlatformState);
            const zone = moduleRef.injector.get(NgZone);
    
            //ADD THIS LINE: this will get the instance of the HttpStatusCodeService created for this request.
            const statusCodeService = moduleRef.injector.get(HttpStatusCodeService); 
    
            return new Promise<RenderResult>((resolve, reject) => {
                zone.onError.subscribe((errorInfo: any) => reject(errorInfo));
                appRef.isStable.first(isStable => isStable).subscribe(() => {
                    // Because 'onStable' fires before 'onError', we have to delay slightly before
                    // completing the request in case there's an error to report
                    setImmediate(() => {
                        resolve({
                            html: state.renderToString(),
    
                            //ADD THIS LINE: this will get the currently set status code and return it along with the prerendered html string
                            statusCode: statusCodeService.getStatusCode() 
                        });
                        moduleRef.destroy();
                    });
                });
            });
        });
    });
    

    And then finally we need to set the status code in any component that shouldn't return HTTP 200:

    import { Component } from '@angular/core';
    
    import { HttpStatusCodeService } from './path/to/services/http-status-code.service';
    
    @Component({
        selector: 'not-found',
        templateUrl: './not-found.html'
    })
    export class NotFoundComponent {
    
      constructor(private httpStatusCodeService: HttpStatusCodeService) {
        httpStatusCodeService.setStatusCode(404);
      }
    }