This service assumes that you followed the SSR receipt at ng-cli (i.e. you use the '@nguniversal/express-engine' package).
Last active
October 20, 2023 20:55
-
-
Save KostyaEsmukov/ce8a6486b2ea596c138770ae393b196f to your computer and use it in GitHub Desktop.
HTTP redirects with Angular SSR
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { PlatformLocation } from '@angular/common'; | |
... | |
@NgModule({ | |
bootstrap: [ AppComponent ], | |
imports: [ | |
... | |
], | |
providers: [ | |
{ provide: PlatformLocation, useClass: ExpressRedirectPlatformLocation }, | |
... | |
], | |
}) | |
export class AppServerModule { } |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | |
} | |
} |
how to use this service in components
i want to 301 redirect based on condition.
@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.
Does this still work with Angular 9.1.9?
Does this still work with Angular 9.1.9?
nope (
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
@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.