[now-routing-utils] Fix error handling on path-to-regexp error (#3523)

This PR fixes an issue where certain patterns caused `path-to-regexp` to throw when it should be returning an error object. The fix is to make sure all inputs work properly with `path-to-regexp` before continuing.
This commit is contained in:
Steven
2020-01-09 10:32:22 -05:00
committed by kodiakhq[bot]
parent 7e584be2cd
commit 9d0b0c6e41
4 changed files with 101 additions and 9 deletions

View File

@@ -0,0 +1,33 @@
# Invalid Custom Route `source`
#### Why This Error Occurred
When defining custom routes a route was added that causes an error during parsing. This can be due to trying to use normal `RegExp` syntax like negative lookaheads (`?!exclude`) without following `path-to-regexp`'s syntax for it.
#### Possible Ways to Fix It
Wrap the `RegExp` part of your `source` as an un-named parameter.
**Before**
```js
{
source: '/feedback/(?!general)',
destination: '/feedback/general'
}
```
**After**
```js
{
source: '/feedback/((?!general).*)',
destination: '/feedback/general'
}
```
### Useful Links
- [path-to-regexp](https://github.com/pillarjs/path-to-regexp/tree/v6.1.0)
- [named parameters](https://github.com/pillarjs/path-to-regexp/blob/v6.1.0/Readme.md#named-parameters)
- [un-named paramters](https://github.com/pillarjs/path-to-regexp/blob/v6.1.0/Readme.md#unnamed-parameters)

View File

@@ -14,6 +14,7 @@ import {
convertRedirects, convertRedirects,
convertHeaders, convertHeaders,
convertTrailingSlash, convertTrailingSlash,
sourceToRegex,
} from './superstatic'; } from './superstatic';
export { getCleanUrls } from './superstatic'; export { getCleanUrls } from './superstatic';
@@ -142,6 +143,18 @@ function checkRegexSyntax(src: string): NowErrorNested | null {
return null; return null;
} }
function checkPatternSyntax(src: string): NowErrorNested | null {
try {
sourceToRegex(src);
} catch (err) {
return {
message: `Invalid pattern: "${src}"`,
src,
};
}
return null;
}
function createNowError( function createNowError(
code: string, code: string,
msg: string, msg: string,
@@ -231,16 +244,29 @@ export function getTransformedRoutes({
if (typeof redirects !== 'undefined') { if (typeof redirects !== 'undefined') {
const code = 'invalid_redirects'; const code = 'invalid_redirects';
const errors = redirects const errorsRegex = redirects
.map(r => checkRegexSyntax(r.source)) .map(r => checkRegexSyntax(r.source))
.filter(notEmpty); .filter(notEmpty);
if (errors.length > 0) { if (errorsRegex.length > 0) {
return { return {
routes, routes,
error: createNowError( error: createNowError(
code, code,
'Redirect `source` contains invalid regex', 'Redirect `source` contains invalid regex. Read more: https://err.sh/now/invalid-route-source',
errors errorsRegex
),
};
}
const errorsPattern = redirects
.map(r => checkPatternSyntax(r.source))
.filter(notEmpty);
if (errorsPattern.length > 0) {
return {
routes,
error: createNowError(
code,
'Redirect `source` contains invalid pattern. Read more: https://err.sh/now/invalid-route-source',
errorsPattern
), ),
}; };
} }
@@ -265,16 +291,29 @@ export function getTransformedRoutes({
if (typeof rewrites !== 'undefined') { if (typeof rewrites !== 'undefined') {
const code = 'invalid_rewrites'; const code = 'invalid_rewrites';
const errors = rewrites const errorsRegex = rewrites
.map(r => checkRegexSyntax(r.source)) .map(r => checkRegexSyntax(r.source))
.filter(notEmpty); .filter(notEmpty);
if (errors.length > 0) { if (errorsRegex.length > 0) {
return { return {
routes, routes,
error: createNowError( error: createNowError(
code, code,
'Rewrites `source` contains invalid regex', 'Rewrites `source` contains invalid regex. Read more: https://err.sh/now/invalid-route-source',
errors errorsRegex
),
};
}
const errorsPattern = rewrites
.map(r => checkPatternSyntax(r.source))
.filter(notEmpty);
if (errorsPattern.length > 0) {
return {
routes,
error: createNowError(
code,
'Rewrites `source` contains invalid pattern. Read more: https://err.sh/now/invalid-route-source',
errorsPattern
), ),
}; };
} }

View File

@@ -101,7 +101,9 @@ export function convertTrailingSlash(enable: boolean, status = 308): Route[] {
return routes; return routes;
} }
function sourceToRegex(source: string): { src: string; segments: string[] } { export function sourceToRegex(
source: string
): { src: string; segments: string[] } {
const keys: Key[] = []; const keys: Key[] = [];
const r = pathToRegexp(source, keys, { strict: true }); const r = pathToRegexp(source, keys, { strict: true });
const segments = keys.map(k => k.name).filter(isString); const segments = keys.map(k => k.name).filter(isString);

View File

@@ -568,6 +568,15 @@ describe('getTransformedRoutes', () => {
assert.equal(actual.error.code, 'invalid_redirects'); assert.equal(actual.error.code, 'invalid_redirects');
}); });
test('should error when redirects is invalid pattern', () => {
const nowConfig = {
redirects: [{ source: '/:?', destination: '/file.html' }],
};
const actual = getTransformedRoutes({ nowConfig });
assert.notEqual(actual.error, null);
assert.equal(actual.error.code, 'invalid_redirects');
});
test('should error when rewrites is invalid regex', () => { test('should error when rewrites is invalid regex', () => {
const nowConfig = { const nowConfig = {
rewrites: [{ source: '^/(*.)\\.html$', destination: '/file.html' }], rewrites: [{ source: '^/(*.)\\.html$', destination: '/file.html' }],
@@ -577,6 +586,15 @@ describe('getTransformedRoutes', () => {
assert.equal(actual.error.code, 'invalid_rewrites'); assert.equal(actual.error.code, 'invalid_rewrites');
}); });
test('should error when rewrites is invalid pattern', () => {
const nowConfig = {
rewrites: [{ source: '/:?', destination: '/file.html' }],
};
const actual = getTransformedRoutes({ nowConfig });
assert.notEqual(actual.error, null);
assert.equal(actual.error.code, 'invalid_rewrites');
});
test('should normalize all redirects before rewrites', () => { test('should normalize all redirects before rewrites', () => {
const nowConfig = { const nowConfig = {
cleanUrls: true, cleanUrls: true,