Skip to content

Instantly share code, notes, and snippets.

@KostyaEsmukov
Last active October 20, 2023 20:55
Show Gist options
  • Select an option

  • Save KostyaEsmukov/ce8a6486b2ea596c138770ae393b196f to your computer and use it in GitHub Desktop.

Select an option

Save KostyaEsmukov/ce8a6486b2ea596c138770ae393b196f to your computer and use it in GitHub Desktop.
HTTP redirects with Angular SSR

HTTP redirects with Angular Server Side Rendering

This service assumes that you followed the SSR receipt at ng-cli (i.e. you use the '@nguniversal/express-engine' package).

import { PlatformLocation } from '@angular/common';
...
@NgModule({
bootstrap: [ AppComponent ],
imports: [
...
],
providers: [
{ provide: PlatformLocation, useClass: ExpressRedirectPlatformLocation },
...
],
})
export class AppServerModule { }
import { Injectable, Inject, Optional } from '@angular/core';
import { Response, Request } from 'express';
import { DOCUMENT } from '@angular/common';
import { INITIAL_CONFIG, ɵINTERNAL_SERVER_PLATFORM_PROVIDERS } from '@angular/platform-server';
import { PlatformLocation } from '@angular/common';
import { REQUEST, RESPONSE } from '@nguniversal/express-engine/tokens';
/**
* This service can't be tested with karma, because tests are run in a browser,
* but `express` is imported here, which requires `http` module of NodeJS.
*/
// https://github.com/angular/angular/issues/13822#issuecomment-283309920
interface IPlatformLocation {
pushState(state: any, title: string, url: string): any;
replaceState(state: any, title: string, url: string): any;
}
// This class is not exported
const ServerPlatformLocation: new(_doc: any, _config: any) => IPlatformLocation =
(ɵINTERNAL_SERVER_PLATFORM_PROVIDERS as any)
.find(provider => provider.provide === PlatformLocation)
.useClass;
/**
* Issue HTTP 302 redirects on internal redirects
*/
@Injectable()
export class ExpressRedirectPlatformLocation extends ServerPlatformLocation {
constructor(
@Inject(DOCUMENT) _doc: any,
@Optional() @Inject(INITIAL_CONFIG) _config: any,
@Inject(REQUEST) private req: Request,
@Inject(RESPONSE) private res: Response,
) {
super(_doc, _config);
}
private redirectExpress(state: any, title: string, url: string) {
if (url === this.req.url) return;
if (this.res.finished) {
const req: any = this.req;
req._r_count = (req._r_count || 0) + 1;
console.warn('Attempted to redirect on a finished response. From',
this.req.url, 'to', url);
if (req._r_count > 10) {
console.error('Detected a redirection loop. killing the nodejs process');
// tslint:disable-next-line
console.trace();
console.log(state, title, url);
process.exit(1);
}
} else {
let status = this.res.statusCode || 0; // attempt to use the already set status
if (status < 300 || status >= 400) status = 302; // temporary redirect
console.log(`Redirecting from ${this.req.url} to ${url} with ${status}`);
this.res.redirect(status, url);
this.res.end();
// I haven't found a way to correctly stop Angular rendering.
// So we just let it end its work, though we have already closed
// the response.
}
}
pushState(state: any, title: string, url: string): any {
this.redirectExpress(state, title, url);
return super.pushState(state, title, url);
}
replaceState(state: any, title: string, url: string): any {
this.redirectExpress(state, title, url);
return super.replaceState(state, title, url);
}
}
@yunus-alkan

yunus-alkan commented Nov 21, 2017

Copy link
Copy Markdown

Thank you, but error: "No provider for REQUEST!"

@KostyaEsmukov

Copy link
Copy Markdown
Author

@yunus-alkan sorry for the late reply, I've never received any notification for your comment, unfortunately.

I've just updated the gist to comply with the currently recommended (by ng-cli) SSR configuration.

The error you were getting was due to request/response objects not being provided under the 'REQUEST'/'RESPONSE' injection tokens (as strings). Now these tokens are explicitly imported from the @nguniversal/express-engine/tokens module.

@bufke

bufke commented Feb 9, 2019

Copy link
Copy Markdown

I'm getting No provider for InjectionToken RESPONSE!

@Noveller

Noveller commented Feb 13, 2019

Copy link
Copy Markdown

I`m getting error after redirection
Unhandled Promise rejection: Can't set headers after they are sent. ; Zone: ; Task: Promise.then ; Value: Error: Can't set headers after they are sent.

@NgModule({
declarations: [
AppComponent,
HomeComponent,
],
imports: [
BrowserModule.withServerTransition({appId: 'my-app'}),
RouterModule.forRoot([
{ path: '', redirectTo: 'lazy', pathMatch: 'full'},
{ path: 'lazy', loadChildren: './lazy/lazy.module#LazyModule'},
{ path: 'lazy/nested', loadChildren: './lazy/lazy.module#LazyModule'}
]),
TransferHttpCacheModule,
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule { }

@fvoska

fvoska commented Feb 19, 2019

Copy link
Copy Markdown

@Noveller I am getting the same error and I think it is unavoidable at the moment, or at least I haven't found a way to avoid it. It happens because of .redirect() and .end(), which sets headers and finishes the response, but angular still continues rendering and tries writing response as well, unaware of any redirects.

Edit: I just found a solution, here it is: https://stackoverflow.com/questions/42067300/angular-2-universal-404-not-found-redirection

I updated my server.ts to look like this:

// All regular routes use the Universal engine
app.get('*', (req: Request, res: Response) => {
  res.render('index', { req, res }, (error: Error, html: string) => {
    if (error) {
      console.error(Date.now(), error);
      res.status(500).send(error);
    } else if (!res.headersSent && !res.finished) {
      res.send(html);
    }
  });
});

and in my Authentication guard I do something like this:

this.response.redirect(`https://${redirectHost}${accessDeniedPath}?redirectUrl=${redirectUrl}`);
this.response.finished = true;
this.response.end();

@oleksmir

Copy link
Copy Markdown

@fvoska It solve issue with unhandled promise rejection. Maybe you know how to handle multiple redirects inside chain of guards? Currently it ends when first response is ended, angular continues rendering and resolving data inside router and I need to continue redirects too.

@NeerajThapliyal

Copy link
Copy Markdown

how to use this service in components
i want to 301 redirect based on condition.

@KostyaEsmukov

Copy link
Copy Markdown
Author

@NeerajThapliyal just use a normal router navigation. https://stackoverflow.com/a/47134257

On server that navigation will be converted to an HTTP redirect by the service provided in this gist.

@btakita

btakita commented May 27, 2020

Copy link
Copy Markdown

Does this still work with Angular 9.1.9?

@OLEKSII-DROZDIUK

Copy link
Copy Markdown

Does this still work with Angular 9.1.9?

nope (

@marcj

marcj commented Oct 20, 2023

Copy link
Copy Markdown

In newer Angular, you can use the Router and check for changed/pushed URLs:

import { Response } from 'express';
import { APP_BOOTSTRAP_LISTENER, ApplicationConfig, mergeApplicationConfig } from '@angular/core';
import { provideServerRendering } from '@angular/platform-server';
import { appConfig } from './app.config';
import { NavigationEnd, Router } from "@angular/router";
import { RESPONSE } from '@nguniversal/express-engine/tokens';

const serverConfig: ApplicationConfig = {
    providers: [
        provideServerRendering(),
        {
            provide: APP_BOOTSTRAP_LISTENER, multi: true, deps: [Router, RESPONSE], useFactory: (router: Router, response: Response) => {
                return () => {
                    router.events.subscribe(event => {
                        //only when redirectTo was used, we redirect via response.redirect(url)
                        if (event instanceof NavigationEnd && event.url !== event.urlAfterRedirects) {
                            response.redirect(301, event.urlAfterRedirects);
                            response.end();
                        }
                    });
                }
            }
        }
    ]
};

export const config = mergeApplicationConfig(appConfig, serverConfig);

you can then either hardcode redirects in your routers:

export const routes: Routes = [
    { path: 'from-this/url', pathMatch: 'full', redirectTo: 'to-this/url'  },
    
    // other routes
];

or use Router.navigate

class MyComponent {
     constructor(private router: Router) {}
     
     
     ngOnInit() {
         this.router.navigate(['to-this/url']);
     }
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment