Skip to content
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 24 additions & 4 deletions packages/router/src/router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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};

Expand Down Expand Up @@ -217,7 +222,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.
Expand Down Expand Up @@ -312,7 +324,7 @@ export class Router {
// run into ngZone
if (!this.locationSubscription) {
this.locationSubscription = <any>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} :
Expand Down Expand Up @@ -490,15 +502,23 @@ 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 {
if (url instanceof UrlTree) {
return containsTree(this.currentUrlTree, url, exact);
}

const urlTree = this.urlSerializer.parse(url);
const urlTree = this.parseUrl(url);

@skryvets skryvets Apr 10, 2018 •

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I changed it to use internal method parseUrl() which has try/catch construction when parsing the URL. Is that qualified as a breaking change?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No.

return containsTree(this.currentUrlTree, urlTree, exact);
}

Expand Down
18 changes: 17 additions & 1 deletion packages/router/src/router_module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import {ChildrenOutletContexts} from './router_outlet_context';
import {NoPreloading, PreloadAllModules, PreloadingStrategy, RouterPreloader} from './router_preloader';
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';


Expand Down Expand Up @@ -300,6 +300,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(
Expand All @@ -322,6 +334,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) => {
Expand Down
42 changes: 41 additions & 1 deletion packages/router/test/integration.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -1010,6 +1010,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);

Expand Down Expand Up @@ -3872,6 +3896,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[]) {
Expand Down
14 changes: 11 additions & 3 deletions packages/router/testing/src/router_testing_module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}
}

Expand Down
2 changes: 2 additions & 0 deletions tools/public_api_guard/router/router.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,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;
Expand Down Expand Up @@ -311,6 +312,7 @@ export declare class Router {
config: Routes;
errorHandler: ErrorHandler;
readonly events: Observable<Event>;
malformedUriErrorHandler: (error: URIError, urlSerializer: UrlSerializer, url: string) => UrlTree;
navigated: boolean;
onSameUrlNavigation: 'reload' | 'ignore';
paramsInheritanceStrategy: 'emptyOnly' | 'always';
Expand Down