From 86d254d3866fee4271c2e643c385dbaa8970b7b5 Mon Sep 17 00:00:00 2001 From: sergeome Date: Tue, 10 Apr 2018 05:01:07 -0500 Subject: [PATCH] fix(router): add ability to recover from malformed url (#23283) Fixes #21468 PR Close #23283 --- packages/router/src/router.ts | 28 +++++++++++-- packages/router/src/router_module.ts | 18 +++++++- packages/router/test/integration.spec.ts | 42 ++++++++++++++++++- .../testing/src/router_testing_module.ts | 14 +++++-- tools/public_api_guard/router/router.d.ts | 2 + 5 files changed, 95 insertions(+), 9 deletions(-) diff --git a/packages/router/src/router.ts b/packages/router/src/router.ts index bbdd53a315..962ffab889 100644 --- a/packages/router/src/router.ts +++ b/packages/router/src/router.ts @@ -160,6 +160,11 @@ function defaultErrorHandler(error: any): any { throw error; } +function defaultMalformedUriErrorHandler( + error: URIError, urlSerializer: UrlSerializer, url: string): UrlTree { + return urlSerializer.parse('/'); +} + type NavStreamValue = boolean | {appliedUrl: UrlTree, snapshot: RouterStateSnapshot, shouldActivate?: boolean}; @@ -230,7 +235,14 @@ export class Router { */ errorHandler: ErrorHandler = defaultErrorHandler; - + /** + * Malformed uri error handler is invoked when `Router.parseUrl(url)` throws an + * error due to containing an invalid character. The most common case would be a `%` sign + * that's not encoded and is not part of a percent encoded sequence. + */ + malformedUriErrorHandler: + (error: URIError, urlSerializer: UrlSerializer, + url: string) => UrlTree = defaultMalformedUriErrorHandler; /** * Indicates if at least one navigation happened. @@ -325,7 +337,7 @@ export class Router { // run into ngZone if (!this.locationSubscription) { this.locationSubscription = this.location.subscribe((change: any) => { - const rawUrlTree = this.urlSerializer.parse(change['url']); + let rawUrlTree = this.parseUrl(change['url']); const source: NavigationTrigger = change['type'] === 'popstate' ? 'popstate' : 'hashchange'; const state = change.state && change.state.navigationId ? {navigationId: change.state.navigationId} : @@ -503,7 +515,15 @@ export class Router { serializeUrl(url: UrlTree): string { return this.urlSerializer.serialize(url); } /** Parses a string into a `UrlTree` */ - parseUrl(url: string): UrlTree { return this.urlSerializer.parse(url); } + parseUrl(url: string): UrlTree { + let urlTree: UrlTree; + try { + urlTree = this.urlSerializer.parse(url); + } catch (e) { + urlTree = this.malformedUriErrorHandler(e, this.urlSerializer, url); + } + return urlTree; + } /** Returns whether the url is activated */ isActive(url: string|UrlTree, exact: boolean): boolean { @@ -511,7 +531,7 @@ export class Router { return containsTree(this.currentUrlTree, url, exact); } - const urlTree = this.urlSerializer.parse(url); + const urlTree = this.parseUrl(url); return containsTree(this.currentUrlTree, urlTree, exact); } diff --git a/packages/router/src/router_module.ts b/packages/router/src/router_module.ts index 22ed2210a3..e1ea750a94 100644 --- a/packages/router/src/router_module.ts +++ b/packages/router/src/router_module.ts @@ -25,7 +25,7 @@ import {NoPreloading, PreloadAllModules, PreloadingStrategy, RouterPreloader} fr import {RouterScroller} from './router_scroller'; import {ActivatedRoute} from './router_state'; import {UrlHandlingStrategy} from './url_handling_strategy'; -import {DefaultUrlSerializer, UrlSerializer} from './url_tree'; +import {DefaultUrlSerializer, UrlSerializer, UrlTree} from './url_tree'; import {flatten} from './utils/collection'; @@ -393,6 +393,18 @@ export interface ExtraOptions { * - `'always'`, enables unconditional inheritance of parent params. */ paramsInheritanceStrategy?: 'emptyOnly'|'always'; + + /** + * A custom malformed uri error handler function. This handler is invoked when encodedURI contains + * invalid character sequences. The default implementation is to redirect to the root url dropping + * any path or param info. This function passes three parameters: + * + * - `'URIError'` - Error thrown when parsing a bad URL + * - `'UrlSerializer'` - UrlSerializer that’s configured with the router. + * - `'url'` - The malformed URL that caused the URIError + * */ + malformedUriErrorHandler?: + (error: URIError, urlSerializer: UrlSerializer, url: string) => UrlTree; } export function setupRouter( @@ -415,6 +427,10 @@ export function setupRouter( router.errorHandler = opts.errorHandler; } + if (opts.malformedUriErrorHandler) { + router.malformedUriErrorHandler = opts.malformedUriErrorHandler; + } + if (opts.enableTracing) { const dom = getDOM(); router.events.subscribe((e: RouterEvent) => { diff --git a/packages/router/test/integration.spec.ts b/packages/router/test/integration.spec.ts index 4d16d5474f..6797458a45 100644 --- a/packages/router/test/integration.spec.ts +++ b/packages/router/test/integration.spec.ts @@ -12,7 +12,7 @@ import {ChangeDetectionStrategy, Component, Injectable, NgModule, NgModuleFactor import {ComponentFixture, TestBed, fakeAsync, inject, tick} from '@angular/core/testing'; import {By} from '@angular/platform-browser/src/dom/debug/by'; import {expect} from '@angular/platform-browser/testing/src/matchers'; -import {ActivatedRoute, ActivatedRouteSnapshot, ActivationEnd, ActivationStart, CanActivate, CanDeactivate, ChildActivationEnd, ChildActivationStart, DetachedRouteHandle, Event, GuardsCheckEnd, GuardsCheckStart, NavigationCancel, NavigationEnd, NavigationError, NavigationStart, PRIMARY_OUTLET, ParamMap, Params, PreloadAllModules, PreloadingStrategy, Resolve, ResolveEnd, ResolveStart, RouteConfigLoadEnd, RouteConfigLoadStart, RouteReuseStrategy, Router, RouterEvent, RouterModule, RouterPreloader, RouterStateSnapshot, RoutesRecognized, RunGuardsAndResolvers, UrlHandlingStrategy, UrlSegmentGroup, UrlTree} from '@angular/router'; +import {ActivatedRoute, ActivatedRouteSnapshot, ActivationEnd, ActivationStart, CanActivate, CanDeactivate, ChildActivationEnd, ChildActivationStart, DetachedRouteHandle, Event, GuardsCheckEnd, GuardsCheckStart, NavigationCancel, NavigationEnd, NavigationError, NavigationStart, PRIMARY_OUTLET, ParamMap, Params, PreloadAllModules, PreloadingStrategy, Resolve, ResolveEnd, ResolveStart, RouteConfigLoadEnd, RouteConfigLoadStart, RouteReuseStrategy, Router, RouterEvent, RouterModule, RouterPreloader, RouterStateSnapshot, RoutesRecognized, RunGuardsAndResolvers, UrlHandlingStrategy, UrlSegmentGroup, UrlSerializer, UrlTree} from '@angular/router'; import {Observable, Observer, of } from 'rxjs'; import {map} from 'rxjs/operators'; @@ -1006,6 +1006,30 @@ describe('Integration', () => { expectEvents(recordedEvents, [[NavigationStart, '/invalid'], [NavigationError, '/invalid']]); }))); + it('should recover from malformed uri errors', + fakeAsync(inject([Router, Location], (router: Router, location: Location) => { + router.resetConfig([{path: 'simple', component: SimpleCmp}]); + const fixture = createRoot(router, RootCmp); + router.navigateByUrl('/invalid/url%with%percent'); + advance(fixture); + expect(location.path()).toEqual('/'); + }))); + + it('should support custom malformed uri error handler', + fakeAsync(inject([Router, Location], (router: Router, location: Location) => { + const customMalformedUriErrorHandler = + (e: URIError, urlSerializer: UrlSerializer, url: string): + UrlTree => { return urlSerializer.parse('/?error=The-URL-you-went-to-is-invalid'); }; + router.malformedUriErrorHandler = customMalformedUriErrorHandler; + + router.resetConfig([{path: 'simple', component: SimpleCmp}]); + + const fixture = createRoot(router, RootCmp); + router.navigateByUrl('/invalid/url%with%percent'); + advance(fixture); + expect(location.path()).toEqual('/?error=The-URL-you-went-to-is-invalid'); + }))); + it('should not swallow errors', fakeAsync(inject([Router], (router: Router) => { const fixture = createRoot(router, RootCmp); @@ -3934,6 +3958,22 @@ describe('Testing router options', () => { expect(router.paramsInheritanceStrategy).toEqual('always'); }))); }); + + describe('malformedUriErrorHandler', () => { + + function malformedUriErrorHandler(e: URIError, urlSerializer: UrlSerializer, url: string) { + return urlSerializer.parse('/error'); + } + + beforeEach(() => { + TestBed.configureTestingModule( + {imports: [RouterTestingModule.withRoutes([], {malformedUriErrorHandler})]}); + }); + + it('should configure the router', fakeAsync(inject([Router], (router: Router) => { + expect(router.malformedUriErrorHandler).toBe(malformedUriErrorHandler); + }))); + }); }); function expectEvents(events: Event[], pairs: any[]) { diff --git a/packages/router/testing/src/router_testing_module.ts b/packages/router/testing/src/router_testing_module.ts index 3ec89852f7..ef4c5ab387 100644 --- a/packages/router/testing/src/router_testing_module.ts +++ b/packages/router/testing/src/router_testing_module.ts @@ -115,12 +115,20 @@ export function setupTestingRouter( opts?: ExtraOptions | UrlHandlingStrategy, urlHandlingStrategy?: UrlHandlingStrategy) { const router = new Router( null !, urlSerializer, contexts, location, injector, loader, compiler, flatten(routes)); - // Handle deprecated argument ordering. if (opts) { + // Handle deprecated argument ordering. if (isUrlHandlingStrategy(opts)) { router.urlHandlingStrategy = opts; - } else if (opts.paramsInheritanceStrategy) { - router.paramsInheritanceStrategy = opts.paramsInheritanceStrategy; + } else { + // Handle ExtraOptions + + if (opts.malformedUriErrorHandler) { + router.malformedUriErrorHandler = opts.malformedUriErrorHandler; + } + + if (opts.paramsInheritanceStrategy) { + router.paramsInheritanceStrategy = opts.paramsInheritanceStrategy; + } } } diff --git a/tools/public_api_guard/router/router.d.ts b/tools/public_api_guard/router/router.d.ts index aa15b85b3b..1236127df2 100644 --- a/tools/public_api_guard/router/router.d.ts +++ b/tools/public_api_guard/router/router.d.ts @@ -115,6 +115,7 @@ export interface ExtraOptions { enableTracing?: boolean; errorHandler?: ErrorHandler; initialNavigation?: InitialNavigation; + malformedUriErrorHandler?: (error: URIError, urlSerializer: UrlSerializer, url: string) => UrlTree; onSameUrlNavigation?: 'reload' | 'ignore'; paramsInheritanceStrategy?: 'emptyOnly' | 'always'; preloadingStrategy?: any; @@ -314,6 +315,7 @@ export declare class Router { config: Routes; errorHandler: ErrorHandler; readonly events: Observable; + malformedUriErrorHandler: (error: URIError, urlSerializer: UrlSerializer, url: string) => UrlTree; navigated: boolean; onSameUrlNavigation: 'reload' | 'ignore'; paramsInheritanceStrategy: 'emptyOnly' | 'always';