Skip to content

Commit 4a42961

Browse files
atscottalxhub
authored andcommitted
feat(router): withNavigationErrorHandler can convert errors to redirects (#55370)
This commit adds the ability to return `RedirectCommand` from the error handler provided by `withNavigationErrorHandler`. This will prevent the error from being surfaced in the `events` observable of the Router and instead convert the error to a redirect. This allows developers to have more control over how the Router handles navigation errors. There are some cases when the application _does not_ want the URL to be reset when an error occurs. resolves #42915 PR Close #55370
1 parent 18a43b5 commit 4a42961

4 files changed

Lines changed: 91 additions & 12 deletions

File tree

‎goldens/public-api/router/index.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1148,7 +1148,7 @@ export function withHashLocation(): RouterHashLocationFeature;
11481148
export function withInMemoryScrolling(options?: InMemoryScrollingOptions): InMemoryScrollingFeature;
11491149

11501150
// @public
1151-
export function withNavigationErrorHandler(handler: (error: NavigationError) => void): NavigationErrorHandlerFeature;
1151+
export function withNavigationErrorHandler(handler: (error: NavigationError) => unknown | RedirectCommand): NavigationErrorHandlerFeature;
11521152

11531153
// @public
11541154
export function withPreloading(preloadingStrategy: Type<PreloadingStrategy>): PreloadingFeature;

‎packages/router/src/navigation_transition.ts‎

Lines changed: 42 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,14 @@ import {
5151
RouteConfigLoadStart,
5252
RoutesRecognized,
5353
} from './events';
54-
import {GuardResult, NavigationBehaviorOptions, QueryParamsHandling, Route, Routes} from './models';
54+
import {
55+
GuardResult,
56+
NavigationBehaviorOptions,
57+
QueryParamsHandling,
58+
RedirectCommand,
59+
Route,
60+
Routes,
61+
} from './models';
5562
import {
5663
isNavigationCancelingError,
5764
isRedirectingNavigationCancelingError,
@@ -324,9 +331,9 @@ interface InternalRouterInterface {
324331
onSameUrlNavigation: 'reload' | 'ignore';
325332
}
326333

327-
export const NAVIGATION_ERROR_HANDLER = new InjectionToken<(error: NavigationError) => void>(
328-
typeof ngDevMode === 'undefined' || ngDevMode ? 'navigation error handler' : '',
329-
);
334+
export const NAVIGATION_ERROR_HANDLER = new InjectionToken<
335+
(error: NavigationError) => unknown | RedirectCommand
336+
>(typeof ngDevMode === 'undefined' || ngDevMode ? 'navigation error handler' : '');
330337

331338
@Injectable({providedIn: 'root'})
332339
export class NavigationTransitions {
@@ -849,13 +856,38 @@ export class NavigationTransitions {
849856
overallTransitionState.targetSnapshot ?? undefined,
850857
);
851858

852-
this.events.next(navigationError);
853859
try {
854-
runInInjectionContext(this.environmentInjector, () =>
855-
this.navigationErrorHandler?.(navigationError),
860+
const navigationErrorHandlerResult = runInInjectionContext(
861+
this.environmentInjector,
862+
() => this.navigationErrorHandler?.(navigationError),
856863
);
857-
const errorHandlerResult = router.errorHandler(e);
858-
overallTransitionState.resolve(!!errorHandlerResult);
864+
865+
if (navigationErrorHandlerResult instanceof RedirectCommand) {
866+
const {message, cancellationCode} = redirectingNavigationError(
867+
this.urlSerializer,
868+
navigationErrorHandlerResult,
869+
);
870+
this.events.next(
871+
new NavigationCancel(
872+
overallTransitionState.id,
873+
this.urlSerializer.serialize(overallTransitionState.extractedUrl),
874+
message,
875+
cancellationCode,
876+
),
877+
);
878+
this.events.next(
879+
new RedirectRequest(
880+
navigationErrorHandlerResult.redirectTo,
881+
navigationErrorHandlerResult.navigationBehaviorOptions,
882+
),
883+
);
884+
} else {
885+
this.events.next(navigationError);
886+
// TODO(atscott): remove deprecation on errorHandler in RouterModule.forRoot and change behavior to provide NAVIGATION_ERROR_HANDLER
887+
// Note: Still remove public `Router.errorHandler` property, as this is supposed to be configured in DI.
888+
const errorHandlerResult = router.errorHandler(e);
889+
overallTransitionState.resolve(!!errorHandlerResult);
890+
}
859891
} catch (ee) {
860892
// TODO(atscott): consider flipping the default behavior of
861893
// resolveNavigationPromiseOnError to be `resolve(false)` when
@@ -873,6 +905,7 @@ export class NavigationTransitions {
873905
}
874906
}
875907
}
908+
876909
return EMPTY;
877910
}),
878911
);

‎packages/router/src/provide_router.ts‎

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ import {of, Subject} from 'rxjs';
3333

3434
import {INPUT_BINDER, RoutedComponentInputBinder} from './directives/router_outlet';
3535
import {Event, NavigationError, stringifyEvent} from './events';
36-
import {Routes} from './models';
36+
import {RedirectCommand, Routes} from './models';
3737
import {NAVIGATION_ERROR_HANDLER, NavigationTransitions} from './navigation_transition';
3838
import {Router} from './router';
3939
import {InMemoryScrollingOptions, ROUTER_CONFIGURATION, RouterConfigOptions} from './router_config';
@@ -648,6 +648,12 @@ export type NavigationErrorHandlerFeature =
648648
* This function is run inside application's [injection context](guide/di/dependency-injection-context)
649649
* so you can use the [`inject`](api/core/inject) function.
650650
*
651+
* This function can return a `RedirectCommand` to convert the error to a redirect, similar to returning
652+
* a `UrlTree` or `RedirectCommand` from a guard. This will also prevent the `Router` from emitting
653+
* `NavigationError`; it will instead emit `NavigationCancel` with code NavigationCancellationCode.Redirect.
654+
* Return values other than `RedirectCommand` are ignored and do not change any behavior with respect to
655+
* how the `Router` handles the error.
656+
*
651657
* @usageNotes
652658
*
653659
* Basic example of how you can use the error handler option:
@@ -672,7 +678,7 @@ export type NavigationErrorHandlerFeature =
672678
* @publicApi
673679
*/
674680
export function withNavigationErrorHandler(
675-
handler: (error: NavigationError) => void,
681+
handler: (error: NavigationError) => unknown | RedirectCommand,
676682
): NavigationErrorHandlerFeature {
677683
const providers = [
678684
{

‎packages/router/test/integration.spec.ts‎

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1897,6 +1897,46 @@ for (const browserAPI of ['navigation', 'history'] as const) {
18971897
expect(TestBed.inject(Handler).handlerCalled).toBeTrue();
18981898
});
18991899

1900+
it('can redirect from error handler', async () => {
1901+
TestBed.configureTestingModule({
1902+
providers: [
1903+
provideRouter(
1904+
[
1905+
{
1906+
path: 'throw',
1907+
canMatch: [
1908+
() => {
1909+
throw new Error('');
1910+
},
1911+
],
1912+
component: BlankCmp,
1913+
},
1914+
{path: 'error', component: BlankCmp},
1915+
],
1916+
withRouterConfig({resolveNavigationPromiseOnError: true}),
1917+
withNavigationErrorHandler(
1918+
() => new RedirectCommand(coreInject(Router).parseUrl('/error')),
1919+
),
1920+
),
1921+
],
1922+
});
1923+
const router = TestBed.inject(Router);
1924+
let emitNavigationError = false;
1925+
let emitNavigationCancelWithRedirect = false;
1926+
router.events.subscribe((e) => {
1927+
if (e instanceof NavigationError) {
1928+
emitNavigationError = true;
1929+
}
1930+
if (e instanceof NavigationCancel && e.code === NavigationCancellationCode.Redirect) {
1931+
emitNavigationCancelWithRedirect = true;
1932+
}
1933+
});
1934+
await router.navigateByUrl('/throw');
1935+
expect(router.url).toEqual('/error');
1936+
expect(emitNavigationError).toBe(false);
1937+
expect(emitNavigationCancelWithRedirect).toBe(true);
1938+
});
1939+
19001940
it('should not break navigation if an error happens in NavigationErrorHandler', async () => {
19011941
TestBed.configureTestingModule({
19021942
providers: [

0 commit comments

Comments
 (0)