Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(router): withNavigationErrorHandler can convert errors to redi… #55370

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 1 addition & 1 deletion goldens/public-api/router/index.md
Expand Up @@ -1148,7 +1148,7 @@ export function withHashLocation(): RouterHashLocationFeature;
export function withInMemoryScrolling(options?: InMemoryScrollingOptions): InMemoryScrollingFeature;

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

// @public
export function withPreloading(preloadingStrategy: Type<PreloadingStrategy>): PreloadingFeature;
Expand Down
51 changes: 42 additions & 9 deletions packages/router/src/navigation_transition.ts
Expand Up @@ -51,7 +51,14 @@ import {
RouteConfigLoadStart,
RoutesRecognized,
} from './events';
import {GuardResult, NavigationBehaviorOptions, QueryParamsHandling, Route, Routes} from './models';
import {
GuardResult,
NavigationBehaviorOptions,
QueryParamsHandling,
RedirectCommand,
Route,
Routes,
} from './models';
import {
isNavigationCancelingError,
isRedirectingNavigationCancelingError,
Expand Down Expand Up @@ -324,9 +331,9 @@ interface InternalRouterInterface {
onSameUrlNavigation: 'reload' | 'ignore';
}

export const NAVIGATION_ERROR_HANDLER = new InjectionToken<(error: NavigationError) => void>(
typeof ngDevMode === 'undefined' || ngDevMode ? 'navigation error handler' : '',
);
export const NAVIGATION_ERROR_HANDLER = new InjectionToken<
(error: NavigationError) => unknown | RedirectCommand
>(typeof ngDevMode === 'undefined' || ngDevMode ? 'navigation error handler' : '');

@Injectable({providedIn: 'root'})
export class NavigationTransitions {
Expand Down Expand Up @@ -849,13 +856,38 @@ export class NavigationTransitions {
overallTransitionState.targetSnapshot ?? undefined,
);

this.events.next(navigationError);
try {
runInInjectionContext(this.environmentInjector, () =>
this.navigationErrorHandler?.(navigationError),
const navigationErrorHandlerResult = runInInjectionContext(
this.environmentInjector,
() => this.navigationErrorHandler?.(navigationError),
);
const errorHandlerResult = router.errorHandler(e);
overallTransitionState.resolve(!!errorHandlerResult);

if (navigationErrorHandlerResult instanceof RedirectCommand) {
const {message, cancellationCode} = redirectingNavigationError(
this.urlSerializer,
navigationErrorHandlerResult,
);
this.events.next(
new NavigationCancel(
overallTransitionState.id,
this.urlSerializer.serialize(overallTransitionState.extractedUrl),
message,
cancellationCode,
),
);
this.events.next(
new RedirectRequest(
navigationErrorHandlerResult.redirectTo,
navigationErrorHandlerResult.navigationBehaviorOptions,
),
);
} else {
this.events.next(navigationError);
// TODO(atscott): remove deprecation on errorHandler in RouterModule.forRoot and change behavior to provide NAVIGATION_ERROR_HANDLER
// Note: Still remove public `Router.errorHandler` property, as this is supposed to be configured in DI.
const errorHandlerResult = router.errorHandler(e);
overallTransitionState.resolve(!!errorHandlerResult);
}
} catch (ee) {
// TODO(atscott): consider flipping the default behavior of
// resolveNavigationPromiseOnError to be `resolve(false)` when
Expand All @@ -873,6 +905,7 @@ export class NavigationTransitions {
}
}
}

return EMPTY;
}),
);
Expand Down
10 changes: 8 additions & 2 deletions packages/router/src/provide_router.ts
Expand Up @@ -33,7 +33,7 @@ import {of, Subject} from 'rxjs';

import {INPUT_BINDER, RoutedComponentInputBinder} from './directives/router_outlet';
import {Event, NavigationError, stringifyEvent} from './events';
import {Routes} from './models';
import {RedirectCommand, Routes} from './models';
import {NAVIGATION_ERROR_HANDLER, NavigationTransitions} from './navigation_transition';
import {Router} from './router';
import {InMemoryScrollingOptions, ROUTER_CONFIGURATION, RouterConfigOptions} from './router_config';
Expand Down Expand Up @@ -648,6 +648,12 @@ export type NavigationErrorHandlerFeature =
* This function is run inside application's [injection context](guide/di/dependency-injection-context)
* so you can use the [`inject`](api/core/inject) function.
*
* This function can return a `RedirectCommand` to convert the error to a redirect, similar to returning
* a `UrlTree` or `RedirectCommand` from a guard. This will also prevent the `Router` from emitting
* `NavigationError`; it will instead emit `NavigationCancel` with code NavigationCancellationCode.Redirect.
* Return values other than `RedirectCommand` are ignored and do not change any behavior with respect to
* how the `Router` handles the error.
*
* @usageNotes
*
* Basic example of how you can use the error handler option:
Expand All @@ -672,7 +678,7 @@ export type NavigationErrorHandlerFeature =
* @publicApi
*/
export function withNavigationErrorHandler(
handler: (error: NavigationError) => void,
handler: (error: NavigationError) => unknown | RedirectCommand,
): NavigationErrorHandlerFeature {
const providers = [
{
Expand Down
40 changes: 40 additions & 0 deletions packages/router/test/integration.spec.ts
Expand Up @@ -1897,6 +1897,46 @@ for (const browserAPI of ['navigation', 'history'] as const) {
expect(TestBed.inject(Handler).handlerCalled).toBeTrue();
});

it('can redirect from error handler', async () => {
TestBed.configureTestingModule({
providers: [
provideRouter(
[
{
path: 'throw',
canMatch: [
() => {
throw new Error('');
},
],
component: BlankCmp,
},
{path: 'error', component: BlankCmp},
],
withRouterConfig({resolveNavigationPromiseOnError: true}),
withNavigationErrorHandler(
() => new RedirectCommand(coreInject(Router).parseUrl('/error')),
),
),
],
});
const router = TestBed.inject(Router);
let emitNavigationError = false;
let emitNavigationCancelWithRedirect = false;
router.events.subscribe((e) => {
if (e instanceof NavigationError) {
emitNavigationError = true;
}
if (e instanceof NavigationCancel && e.code === NavigationCancellationCode.Redirect) {
emitNavigationCancelWithRedirect = true;
}
});
await router.navigateByUrl('/throw');
expect(router.url).toEqual('/error');
expect(emitNavigationError).toBe(false);
expect(emitNavigationCancelWithRedirect).toBe(true);
});

it('should not break navigation if an error happens in NavigationErrorHandler', async () => {
TestBed.configureTestingModule({
providers: [
Expand Down