fix(router): add ability to recover from malformed url (#23283)
Fixes #21468 PR Close #23283
This commit is contained in:
parent
505b54b86b
commit
86d254d386
|
@ -160,6 +160,11 @@ function defaultErrorHandler(error: any): any {
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function defaultMalformedUriErrorHandler(
|
||||||
|
error: URIError, urlSerializer: UrlSerializer, url: string): UrlTree {
|
||||||
|
return urlSerializer.parse('/');
|
||||||
|
}
|
||||||
|
|
||||||
type NavStreamValue =
|
type NavStreamValue =
|
||||||
boolean | {appliedUrl: UrlTree, snapshot: RouterStateSnapshot, shouldActivate?: boolean};
|
boolean | {appliedUrl: UrlTree, snapshot: RouterStateSnapshot, shouldActivate?: boolean};
|
||||||
|
|
||||||
|
@ -230,7 +235,14 @@ export class Router {
|
||||||
*/
|
*/
|
||||||
errorHandler: ErrorHandler = defaultErrorHandler;
|
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.
|
* Indicates if at least one navigation happened.
|
||||||
|
@ -325,7 +337,7 @@ export class Router {
|
||||||
// run into ngZone
|
// run into ngZone
|
||||||
if (!this.locationSubscription) {
|
if (!this.locationSubscription) {
|
||||||
this.locationSubscription = <any>this.location.subscribe((change: any) => {
|
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 source: NavigationTrigger = change['type'] === 'popstate' ? 'popstate' : 'hashchange';
|
||||||
const state = change.state && change.state.navigationId ?
|
const state = change.state && change.state.navigationId ?
|
||||||
{navigationId: change.state.navigationId} :
|
{navigationId: change.state.navigationId} :
|
||||||
|
@ -503,7 +515,15 @@ export class Router {
|
||||||
serializeUrl(url: UrlTree): string { return this.urlSerializer.serialize(url); }
|
serializeUrl(url: UrlTree): string { return this.urlSerializer.serialize(url); }
|
||||||
|
|
||||||
/** Parses a string into a `UrlTree` */
|
/** 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 */
|
/** Returns whether the url is activated */
|
||||||
isActive(url: string|UrlTree, exact: boolean): boolean {
|
isActive(url: string|UrlTree, exact: boolean): boolean {
|
||||||
|
@ -511,7 +531,7 @@ export class Router {
|
||||||
return containsTree(this.currentUrlTree, url, exact);
|
return containsTree(this.currentUrlTree, url, exact);
|
||||||
}
|
}
|
||||||
|
|
||||||
const urlTree = this.urlSerializer.parse(url);
|
const urlTree = this.parseUrl(url);
|
||||||
return containsTree(this.currentUrlTree, urlTree, exact);
|
return containsTree(this.currentUrlTree, urlTree, exact);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -25,7 +25,7 @@ import {NoPreloading, PreloadAllModules, PreloadingStrategy, RouterPreloader} fr
|
||||||
import {RouterScroller} from './router_scroller';
|
import {RouterScroller} from './router_scroller';
|
||||||
import {ActivatedRoute} from './router_state';
|
import {ActivatedRoute} from './router_state';
|
||||||
import {UrlHandlingStrategy} from './url_handling_strategy';
|
import {UrlHandlingStrategy} from './url_handling_strategy';
|
||||||
import {DefaultUrlSerializer, UrlSerializer} from './url_tree';
|
import {DefaultUrlSerializer, UrlSerializer, UrlTree} from './url_tree';
|
||||||
import {flatten} from './utils/collection';
|
import {flatten} from './utils/collection';
|
||||||
|
|
||||||
|
|
||||||
|
@ -393,6 +393,18 @@ export interface ExtraOptions {
|
||||||
* - `'always'`, enables unconditional inheritance of parent params.
|
* - `'always'`, enables unconditional inheritance of parent params.
|
||||||
*/
|
*/
|
||||||
paramsInheritanceStrategy?: 'emptyOnly'|'always';
|
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(
|
export function setupRouter(
|
||||||
|
@ -415,6 +427,10 @@ export function setupRouter(
|
||||||
router.errorHandler = opts.errorHandler;
|
router.errorHandler = opts.errorHandler;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (opts.malformedUriErrorHandler) {
|
||||||
|
router.malformedUriErrorHandler = opts.malformedUriErrorHandler;
|
||||||
|
}
|
||||||
|
|
||||||
if (opts.enableTracing) {
|
if (opts.enableTracing) {
|
||||||
const dom = getDOM();
|
const dom = getDOM();
|
||||||
router.events.subscribe((e: RouterEvent) => {
|
router.events.subscribe((e: RouterEvent) => {
|
||||||
|
|
|
@ -12,7 +12,7 @@ import {ChangeDetectionStrategy, Component, Injectable, NgModule, NgModuleFactor
|
||||||
import {ComponentFixture, TestBed, fakeAsync, inject, tick} from '@angular/core/testing';
|
import {ComponentFixture, TestBed, fakeAsync, inject, tick} from '@angular/core/testing';
|
||||||
import {By} from '@angular/platform-browser/src/dom/debug/by';
|
import {By} from '@angular/platform-browser/src/dom/debug/by';
|
||||||
import {expect} from '@angular/platform-browser/testing/src/matchers';
|
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 {Observable, Observer, of } from 'rxjs';
|
||||||
import {map} from 'rxjs/operators';
|
import {map} from 'rxjs/operators';
|
||||||
|
|
||||||
|
@ -1006,6 +1006,30 @@ describe('Integration', () => {
|
||||||
expectEvents(recordedEvents, [[NavigationStart, '/invalid'], [NavigationError, '/invalid']]);
|
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) => {
|
it('should not swallow errors', fakeAsync(inject([Router], (router: Router) => {
|
||||||
const fixture = createRoot(router, RootCmp);
|
const fixture = createRoot(router, RootCmp);
|
||||||
|
|
||||||
|
@ -3934,6 +3958,22 @@ describe('Testing router options', () => {
|
||||||
expect(router.paramsInheritanceStrategy).toEqual('always');
|
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[]) {
|
function expectEvents(events: Event[], pairs: any[]) {
|
||||||
|
|
|
@ -115,12 +115,20 @@ export function setupTestingRouter(
|
||||||
opts?: ExtraOptions | UrlHandlingStrategy, urlHandlingStrategy?: UrlHandlingStrategy) {
|
opts?: ExtraOptions | UrlHandlingStrategy, urlHandlingStrategy?: UrlHandlingStrategy) {
|
||||||
const router = new Router(
|
const router = new Router(
|
||||||
null !, urlSerializer, contexts, location, injector, loader, compiler, flatten(routes));
|
null !, urlSerializer, contexts, location, injector, loader, compiler, flatten(routes));
|
||||||
// Handle deprecated argument ordering.
|
|
||||||
if (opts) {
|
if (opts) {
|
||||||
|
// Handle deprecated argument ordering.
|
||||||
if (isUrlHandlingStrategy(opts)) {
|
if (isUrlHandlingStrategy(opts)) {
|
||||||
router.urlHandlingStrategy = opts;
|
router.urlHandlingStrategy = opts;
|
||||||
} else if (opts.paramsInheritanceStrategy) {
|
} else {
|
||||||
router.paramsInheritanceStrategy = opts.paramsInheritanceStrategy;
|
// Handle ExtraOptions
|
||||||
|
|
||||||
|
if (opts.malformedUriErrorHandler) {
|
||||||
|
router.malformedUriErrorHandler = opts.malformedUriErrorHandler;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (opts.paramsInheritanceStrategy) {
|
||||||
|
router.paramsInheritanceStrategy = opts.paramsInheritanceStrategy;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -115,6 +115,7 @@ export interface ExtraOptions {
|
||||||
enableTracing?: boolean;
|
enableTracing?: boolean;
|
||||||
errorHandler?: ErrorHandler;
|
errorHandler?: ErrorHandler;
|
||||||
initialNavigation?: InitialNavigation;
|
initialNavigation?: InitialNavigation;
|
||||||
|
malformedUriErrorHandler?: (error: URIError, urlSerializer: UrlSerializer, url: string) => UrlTree;
|
||||||
onSameUrlNavigation?: 'reload' | 'ignore';
|
onSameUrlNavigation?: 'reload' | 'ignore';
|
||||||
paramsInheritanceStrategy?: 'emptyOnly' | 'always';
|
paramsInheritanceStrategy?: 'emptyOnly' | 'always';
|
||||||
preloadingStrategy?: any;
|
preloadingStrategy?: any;
|
||||||
|
@ -314,6 +315,7 @@ export declare class Router {
|
||||||
config: Routes;
|
config: Routes;
|
||||||
errorHandler: ErrorHandler;
|
errorHandler: ErrorHandler;
|
||||||
readonly events: Observable<Event>;
|
readonly events: Observable<Event>;
|
||||||
|
malformedUriErrorHandler: (error: URIError, urlSerializer: UrlSerializer, url: string) => UrlTree;
|
||||||
navigated: boolean;
|
navigated: boolean;
|
||||||
onSameUrlNavigation: 'reload' | 'ignore';
|
onSameUrlNavigation: 'reload' | 'ignore';
|
||||||
paramsInheritanceStrategy: 'emptyOnly' | 'always';
|
paramsInheritanceStrategy: 'emptyOnly' | 'always';
|
||||||
|
|
Loading…
Reference in New Issue