Skip to content

Commit 2d4f4b5

Browse files
sergeomemhevery
authored andcommitted
fix(router): add ability to recover from malformed url (#23283)
Fixes #21468 PR Close #23283
1 parent 912742f commit 2d4f4b5

File tree

5 files changed

+95
-9
lines changed

5 files changed

+95
-9
lines changed

packages/router/src/router.ts

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,11 @@ function defaultErrorHandler(error: any): any {
160160
throw error;
161161
}
162162

163+
function defaultMalformedUriErrorHandler(
164+
error: URIError, urlSerializer: UrlSerializer, url: string): UrlTree {
165+
return urlSerializer.parse('/');
166+
}
167+
163168
type NavStreamValue =
164169
boolean | {appliedUrl: UrlTree, snapshot: RouterStateSnapshot, shouldActivate?: boolean};
165170

@@ -217,7 +222,14 @@ export class Router {
217222
*/
218223
errorHandler: ErrorHandler = defaultErrorHandler;
219224

220-
225+
/**
226+
* Malformed uri error handler is invoked when `Router.parseUrl(url)` throws an
227+
* error due to containing an invalid character. The most common case would be a `%` sign
228+
* that's not encoded and is not part of a percent encoded sequence.
229+
*/
230+
malformedUriErrorHandler:
231+
(error: URIError, urlSerializer: UrlSerializer,
232+
url: string) => UrlTree = defaultMalformedUriErrorHandler;
221233

222234
/**
223235
* Indicates if at least one navigation happened.
@@ -312,7 +324,7 @@ export class Router {
312324
// run into ngZone
313325
if (!this.locationSubscription) {
314326
this.locationSubscription = <any>this.location.subscribe((change: any) => {
315-
const rawUrlTree = this.urlSerializer.parse(change['url']);
327+
let rawUrlTree = this.parseUrl(change['url']);
316328
const source: NavigationTrigger = change['type'] === 'popstate' ? 'popstate' : 'hashchange';
317329
const state = change.state && change.state.navigationId ?
318330
{navigationId: change.state.navigationId} :
@@ -490,15 +502,23 @@ export class Router {
490502
serializeUrl(url: UrlTree): string { return this.urlSerializer.serialize(url); }
491503

492504
/** Parses a string into a `UrlTree` */
493-
parseUrl(url: string): UrlTree { return this.urlSerializer.parse(url); }
505+
parseUrl(url: string): UrlTree {
506+
let urlTree: UrlTree;
507+
try {
508+
urlTree = this.urlSerializer.parse(url);
509+
} catch (e) {
510+
urlTree = this.malformedUriErrorHandler(e, this.urlSerializer, url);
511+
}
512+
return urlTree;
513+
}
494514

495515
/** Returns whether the url is activated */
496516
isActive(url: string|UrlTree, exact: boolean): boolean {
497517
if (url instanceof UrlTree) {
498518
return containsTree(this.currentUrlTree, url, exact);
499519
}
500520

501-
const urlTree = this.urlSerializer.parse(url);
521+
const urlTree = this.parseUrl(url);
502522
return containsTree(this.currentUrlTree, urlTree, exact);
503523
}
504524

packages/router/src/router_module.ts

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ import {ChildrenOutletContexts} from './router_outlet_context';
2424
import {NoPreloading, PreloadAllModules, PreloadingStrategy, RouterPreloader} from './router_preloader';
2525
import {ActivatedRoute} from './router_state';
2626
import {UrlHandlingStrategy} from './url_handling_strategy';
27-
import {DefaultUrlSerializer, UrlSerializer} from './url_tree';
27+
import {DefaultUrlSerializer, UrlSerializer, UrlTree} from './url_tree';
2828
import {flatten} from './utils/collection';
2929

3030

@@ -308,6 +308,18 @@ export interface ExtraOptions {
308308
* - `'always'`, enables unconditional inheritance of parent params.
309309
*/
310310
paramsInheritanceStrategy?: 'emptyOnly'|'always';
311+
312+
/**
313+
* A custom malformed uri error handler function. This handler is invoked when encodedURI contains
314+
* invalid character sequences. The default implementation is to redirect to the root url dropping
315+
* any path or param info. This function passes three parameters:
316+
*
317+
* - `'URIError'` - Error thrown when parsing a bad URL
318+
* - `'UrlSerializer'` - UrlSerializer that’s configured with the router.
319+
* - `'url'` - The malformed URL that caused the URIError
320+
* */
321+
malformedUriErrorHandler?:
322+
(error: URIError, urlSerializer: UrlSerializer, url: string) => UrlTree;
311323
}
312324

313325
export function setupRouter(
@@ -330,6 +342,10 @@ export function setupRouter(
330342
router.errorHandler = opts.errorHandler;
331343
}
332344

345+
if (opts.malformedUriErrorHandler) {
346+
router.malformedUriErrorHandler = opts.malformedUriErrorHandler;
347+
}
348+
333349
if (opts.enableTracing) {
334350
const dom = getDOM();
335351
router.events.subscribe((e: RouterEvent) => {

packages/router/test/integration.spec.ts

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import {ChangeDetectionStrategy, Component, Injectable, NgModule, NgModuleFactor
1212
import {ComponentFixture, TestBed, fakeAsync, inject, tick} from '@angular/core/testing';
1313
import {By} from '@angular/platform-browser/src/dom/debug/by';
1414
import {expect} from '@angular/platform-browser/testing/src/matchers';
15-
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';
15+
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';
1616
import {Observable, Observer, of } from 'rxjs';
1717
import {map} from 'rxjs/operators';
1818

@@ -1010,6 +1010,30 @@ describe('Integration', () => {
10101010
expectEvents(recordedEvents, [[NavigationStart, '/invalid'], [NavigationError, '/invalid']]);
10111011
})));
10121012

1013+
it('should recover from malformed uri errors',
1014+
fakeAsync(inject([Router, Location], (router: Router, location: Location) => {
1015+
router.resetConfig([{path: 'simple', component: SimpleCmp}]);
1016+
const fixture = createRoot(router, RootCmp);
1017+
router.navigateByUrl('/invalid/url%with%percent');
1018+
advance(fixture);
1019+
expect(location.path()).toEqual('/');
1020+
})));
1021+
1022+
it('should support custom malformed uri error handler',
1023+
fakeAsync(inject([Router, Location], (router: Router, location: Location) => {
1024+
const customMalformedUriErrorHandler =
1025+
(e: URIError, urlSerializer: UrlSerializer, url: string):
1026+
UrlTree => { return urlSerializer.parse('/?error=The-URL-you-went-to-is-invalid'); };
1027+
router.malformedUriErrorHandler = customMalformedUriErrorHandler;
1028+
1029+
router.resetConfig([{path: 'simple', component: SimpleCmp}]);
1030+
1031+
const fixture = createRoot(router, RootCmp);
1032+
router.navigateByUrl('/invalid/url%with%percent');
1033+
advance(fixture);
1034+
expect(location.path()).toEqual('/?error=The-URL-you-went-to-is-invalid');
1035+
})));
1036+
10131037
it('should not swallow errors', fakeAsync(inject([Router], (router: Router) => {
10141038
const fixture = createRoot(router, RootCmp);
10151039

@@ -3938,6 +3962,22 @@ describe('Testing router options', () => {
39383962
expect(router.paramsInheritanceStrategy).toEqual('always');
39393963
})));
39403964
});
3965+
3966+
describe('malformedUriErrorHandler', () => {
3967+
3968+
function malformedUriErrorHandler(e: URIError, urlSerializer: UrlSerializer, url: string) {
3969+
return urlSerializer.parse('/error');
3970+
}
3971+
3972+
beforeEach(() => {
3973+
TestBed.configureTestingModule(
3974+
{imports: [RouterTestingModule.withRoutes([], {malformedUriErrorHandler})]});
3975+
});
3976+
3977+
it('should configure the router', fakeAsync(inject([Router], (router: Router) => {
3978+
expect(router.malformedUriErrorHandler).toBe(malformedUriErrorHandler);
3979+
})));
3980+
});
39413981
});
39423982

39433983
function expectEvents(events: Event[], pairs: any[]) {

packages/router/testing/src/router_testing_module.ts

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -115,12 +115,20 @@ export function setupTestingRouter(
115115
opts?: ExtraOptions | UrlHandlingStrategy, urlHandlingStrategy?: UrlHandlingStrategy) {
116116
const router = new Router(
117117
null !, urlSerializer, contexts, location, injector, loader, compiler, flatten(routes));
118-
// Handle deprecated argument ordering.
119118
if (opts) {
119+
// Handle deprecated argument ordering.
120120
if (isUrlHandlingStrategy(opts)) {
121121
router.urlHandlingStrategy = opts;
122-
} else if (opts.paramsInheritanceStrategy) {
123-
router.paramsInheritanceStrategy = opts.paramsInheritanceStrategy;
122+
} else {
123+
// Handle ExtraOptions
124+
125+
if (opts.malformedUriErrorHandler) {
126+
router.malformedUriErrorHandler = opts.malformedUriErrorHandler;
127+
}
128+
129+
if (opts.paramsInheritanceStrategy) {
130+
router.paramsInheritanceStrategy = opts.paramsInheritanceStrategy;
131+
}
124132
}
125133
}
126134

tools/public_api_guard/router/router.d.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,7 @@ export interface ExtraOptions {
114114
enableTracing?: boolean;
115115
errorHandler?: ErrorHandler;
116116
initialNavigation?: InitialNavigation;
117+
malformedUriErrorHandler?: (error: URIError, urlSerializer: UrlSerializer, url: string) => UrlTree;
117118
onSameUrlNavigation?: 'reload' | 'ignore';
118119
paramsInheritanceStrategy?: 'emptyOnly' | 'always';
119120
preloadingStrategy?: any;
@@ -311,6 +312,7 @@ export declare class Router {
311312
config: Routes;
312313
errorHandler: ErrorHandler;
313314
readonly events: Observable<Event>;
315+
malformedUriErrorHandler: (error: URIError, urlSerializer: UrlSerializer, url: string) => UrlTree;
314316
navigated: boolean;
315317
onSameUrlNavigation: 'reload' | 'ignore';
316318
paramsInheritanceStrategy: 'emptyOnly' | 'always';

0 commit comments

Comments
 (0)