[next][routing-utils] Add missing matcher support (#8874)

### Related Issues

This adds handling to ensure we pass through the `missing` route field
correctly for custom routes and middleware matchers. Tests are also
added in the complimentary Next.js PR for this, example deployment can
be seen here
https://vtest314-e2e-tests-jj4-vtest314-next-e2e-tests.vercel.app/

x-ref: [slack
thread](https://vercel.slack.com/archives/C03S8ED1DKM/p1667935428788529?thread_ts=1667850697.542269&cid=C03S8ED1DKM)
x-ref: https://github.com/vercel/next.js/pull/42660

### 📋 Checklist

<!--
  Please keep your PR as a Draft until the checklist is complete
-->

#### Tests

- [ ] The code changed/added as part of this PR has been covered with
tests
- [ ] All tests pass locally with `yarn test-unit`

#### Code Review

- [ ] This PR has a concise title and thorough description useful to a
reviewer
- [ ] Issue from task tracker has a link to this PR
This commit is contained in:
JJ Kasper
2022-11-08 16:17:34 -08:00
committed by GitHub
parent fbb8bba4cf
commit c2d0887b94
3 changed files with 180 additions and 4 deletions

View File

@@ -2278,6 +2278,7 @@ interface EdgeFunctionInfoV2 extends BaseEdgeFunctionInfo {
interface EdgeFunctionMatcher {
regexp: string;
has?: HasField;
missing?: HasField;
}
export async function getMiddlewareBundle({
@@ -2479,6 +2480,7 @@ export async function getMiddlewareBundle({
key: 'x-prerender-revalidate',
value: prerenderBypassToken,
},
...(matcher.missing || []),
],
};
@@ -2609,6 +2611,9 @@ function getRouteMatchers(
if (matcher.has) {
m.has = normalizeHas(matcher.has);
}
if (matcher.missing) {
m.missing = normalizeHas(matcher.missing);
}
return m;
});
}

View File

@@ -51,6 +51,9 @@ export function convertRedirects(
return redirects.map(r => {
const { src, segments } = sourceToRegex(r.source);
const hasSegments = collectHasSegments(r.has);
normalizeHasKeys(r.has);
normalizeHasKeys(r.missing);
try {
const loc = replaceSegments(segments, hasSegments, r.destination, true);
let status: number;
@@ -70,6 +73,9 @@ export function convertRedirects(
if (r.has) {
route.has = r.has;
}
if (r.missing) {
route.missing = r.missing;
}
return route;
} catch (e) {
throw new Error(`Failed to parse redirect: ${JSON.stringify(r)}`);
@@ -84,6 +90,9 @@ export function convertRewrites(
return rewrites.map(r => {
const { src, segments } = sourceToRegex(r.source);
const hasSegments = collectHasSegments(r.has);
normalizeHasKeys(r.has);
normalizeHasKeys(r.missing);
try {
const dest = replaceSegments(
segments,
@@ -97,6 +106,9 @@ export function convertRewrites(
if (r.has) {
route.has = r.has;
}
if (r.missing) {
route.missing = r.missing;
}
return route;
} catch (e) {
throw new Error(`Failed to parse rewrite: ${JSON.stringify(r)}`);
@@ -109,6 +121,9 @@ export function convertHeaders(headers: Header[]): Route[] {
const obj: { [key: string]: string } = {};
const { src, segments } = sourceToRegex(h.source);
const hasSegments = collectHasSegments(h.has);
normalizeHasKeys(h.has);
normalizeHasKeys(h.missing);
const namedSegments = segments.filter(name => name !== UN_NAMED_SEGMENT);
const indexes: { [k: string]: string } = {};
@@ -140,6 +155,9 @@ export function convertHeaders(headers: Header[]): Route[] {
if (h.has) {
route.has = h.has;
}
if (h.missing) {
route.missing = h.missing;
}
return route;
});
}
@@ -193,14 +211,19 @@ export function sourceToRegex(source: string): {
const namedGroupsRegex = /\(\?<([a-zA-Z][a-zA-Z0-9]*)>/g;
const normalizeHasKeys = (hasItems: HasField = []) => {
for (const hasItem of hasItems) {
if ('key' in hasItem && hasItem.type === 'header') {
hasItem.key = hasItem.key.toLowerCase();
}
}
return hasItems;
};
export function collectHasSegments(has?: HasField) {
const hasSegments = new Set<string>();
for (const hasItem of has || []) {
if ('key' in hasItem && hasItem.type === 'header') {
hasItem.key = hasItem.key.toLowerCase();
}
if (!hasItem.value && 'key' in hasItem) {
hasSegments.add(hasItem.key);
}

View File

@@ -224,6 +224,17 @@ test('convertRedirects', () => {
],
permanent: false,
},
{
source: '/hello/:first',
destination: '/another',
missing: [
{
type: 'host',
value: '(?<a>.*)\\.(?<b>.*)',
},
],
permanent: false,
},
{
source: '/hello/:first',
destination:
@@ -384,6 +395,19 @@ test('convertRedirects', () => {
src: '^\\/hello(?:\\/([^\\/]+?))$',
status: 307,
},
{
missing: [
{
type: 'host',
value: '(?<a>.*)\\.(?<b>.*)',
},
],
headers: {
Location: '/another',
},
src: '^\\/hello(?:\\/([^\\/]+?))$',
status: 307,
},
{
has: [
{
@@ -453,6 +477,7 @@ test('convertRedirects', () => {
['/hello/world', '/hello/again'],
['/hello/world'],
['/hello/world'],
['/hello/world'],
];
const mustNotMatch = [
@@ -474,6 +499,7 @@ test('convertRedirects', () => {
['/feature/first', '/feature'],
['/hello', '/hello/another/one'],
['/hellooo'],
['/hellooo'],
['/helloooo'],
];
@@ -586,6 +612,48 @@ test('convertRewrites', () => {
},
],
},
{
source: '/hello/:first',
destination: '/another',
missing: [
{
type: 'header',
key: 'x-rewrite',
},
{
type: 'cookie',
key: 'loggedIn',
value: '1',
},
{
type: 'host',
value: 'vercel.com',
},
{
type: 'host',
value: '(?<a>.*)\\.(?<b>.*)',
},
{
type: 'header',
key: 'host',
value: '(?<c>.*)\\.(?<d>.*)',
},
{
type: 'query',
key: 'username',
},
{
type: 'header',
key: 'x-pathname',
value: '(?<pathname>.*)',
},
{
type: 'header',
key: 'X-Pathname',
value: '(?<another>hello|world)',
},
],
},
{
source: '/array-query-string/:id/:name',
destination: 'https://example.com/?tag=1&tag=2',
@@ -740,6 +808,49 @@ test('convertRewrites', () => {
],
src: '^\\/hello(?:\\/([^\\/]+?))$',
},
{
check: true,
dest: '/another?first=$1',
missing: [
{
key: 'x-rewrite',
type: 'header',
},
{
key: 'loggedIn',
type: 'cookie',
value: '1',
},
{
type: 'host',
value: 'vercel.com',
},
{
type: 'host',
value: '(?<a>.*)\\.(?<b>.*)',
},
{
key: 'host',
type: 'header',
value: '(?<c>.*)\\.(?<d>.*)',
},
{
type: 'query',
key: 'username',
},
{
type: 'header',
key: 'x-pathname',
value: '(?<pathname>.*)',
},
{
type: 'header',
key: 'x-pathname',
value: '(?<another>hello|world)',
},
],
src: '^\\/hello(?:\\/([^\\/]+?))$',
},
{
src: '^\\/array-query-string(?:\\/([^\\/]+?))(?:\\/([^\\/]+?))$',
dest: 'https://example.com/?tag=1&tag=2&id=$1&name=$2',
@@ -776,6 +887,7 @@ test('convertRewrites', () => {
['/hello/world', '/hello/again'],
['/hello/world'],
['/hello/world'],
['/hello/world'],
['/array-query-string/10/email'],
['/en/hello'],
];
@@ -802,6 +914,7 @@ test('convertRewrites', () => {
['/hello', '/hello/another/one'],
['/hllooo'],
['/hllooo'],
['/hllooo'],
['/array-query-string/10'],
['/en/hello/world', '/en/hello/'],
];
@@ -919,6 +1032,25 @@ test('convertHeaders', () => {
},
],
},
{
source: '/hello/:first',
missing: [
{
type: 'host',
value: '(?<a>.*)\\.(?<b>.*)',
},
],
headers: [
{
key: 'x-a',
value: 'a',
},
{
key: 'x-b',
value: 'b',
},
],
},
{
source: '/hello/:first',
has: [
@@ -1057,6 +1189,20 @@ test('convertHeaders', () => {
},
src: '^\\/hello(?:\\/([^\\/]+?))$',
},
{
continue: true,
missing: [
{
type: 'host',
value: '(?<a>.*)\\.(?<b>.*)',
},
],
headers: {
'x-a': 'a',
'x-b': 'b',
},
src: '^\\/hello(?:\\/([^\\/]+?))$',
},
{
continue: true,
has: [
@@ -1133,6 +1279,7 @@ test('convertHeaders', () => {
['/like/params/first', '/like/params/second'],
['/hello/world'],
['/hello/world'],
['/hello/world'],
['/hello'],
];
@@ -1143,6 +1290,7 @@ test('convertHeaders', () => {
['/non-match', '/like/params', '/like/params/'],
['/hellooo'],
['/hellooo'],
['/hellooo'],
[],
];