@opennextjs/aws 4.1.8 → 4.1.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (101) hide show
  1. package/dist/adapters/cache.d.ts +1 -1
  2. package/dist/adapters/cache.js +7 -6
  3. package/dist/adapters/composable-cache.d.ts +1 -1
  4. package/dist/adapters/composable-cache.js +2 -2
  5. package/dist/adapters/config/util.d.ts +10 -10
  6. package/dist/adapters/edge-adapter.js +2 -2
  7. package/dist/adapters/middleware.js +2 -2
  8. package/dist/build/copyTracedFiles.js +6 -2
  9. package/dist/build/edge/createEdgeBundle.d.ts +2 -2
  10. package/dist/build/installDeps.d.ts +1 -1
  11. package/dist/build/validateConfig.d.ts +1 -1
  12. package/dist/core/createGenericHandler.d.ts +2 -2
  13. package/dist/core/edgeFunctionHandler.d.ts +1 -1
  14. package/dist/core/nodeMiddlewareHandler.d.ts +1 -1
  15. package/dist/core/requestHandler.d.ts +2 -2
  16. package/dist/core/requestHandler.js +15 -7
  17. package/dist/core/resolve.d.ts +10 -10
  18. package/dist/core/routing/cacheInterceptor.d.ts +1 -1
  19. package/dist/core/routing/cacheInterceptor.js +6 -6
  20. package/dist/core/routing/i18n/index.d.ts +2 -2
  21. package/dist/core/routing/matcher.d.ts +61 -6
  22. package/dist/core/routing/matcher.js +283 -55
  23. package/dist/core/routing/middleware.d.ts +8 -4
  24. package/dist/core/routing/middleware.js +29 -12
  25. package/dist/core/routing/routeMatcher.d.ts +1 -1
  26. package/dist/core/routing/routeMatcher.js +1 -1
  27. package/dist/core/routing/util.d.ts +20 -4
  28. package/dist/core/routing/util.js +32 -15
  29. package/dist/core/routingHandler.d.ts +2 -2
  30. package/dist/core/routingHandler.js +3 -3
  31. package/dist/helpers/withCloudflare.d.ts +1 -1
  32. package/dist/helpers/withSST.d.ts +2 -2
  33. package/dist/http/openNextResponse.d.ts +1 -1
  34. package/dist/overrides/assetResolver/dummy.d.ts +1 -1
  35. package/dist/overrides/converters/aws-apigw-v1.d.ts +1 -1
  36. package/dist/overrides/converters/aws-apigw-v1.js +1 -1
  37. package/dist/overrides/converters/aws-apigw-v2.d.ts +1 -1
  38. package/dist/overrides/converters/aws-apigw-v2.js +2 -2
  39. package/dist/overrides/converters/aws-cloudfront.d.ts +1 -1
  40. package/dist/overrides/converters/aws-cloudfront.js +2 -2
  41. package/dist/overrides/converters/dummy.d.ts +1 -1
  42. package/dist/overrides/converters/edge.d.ts +2 -2
  43. package/dist/overrides/converters/edge.js +1 -1
  44. package/dist/overrides/converters/node.d.ts +1 -1
  45. package/dist/overrides/converters/sqs-revalidate.d.ts +1 -1
  46. package/dist/overrides/imageLoader/dummy.d.ts +1 -1
  47. package/dist/overrides/imageLoader/dummy.js +1 -1
  48. package/dist/overrides/imageLoader/fs-dev.js +2 -2
  49. package/dist/overrides/imageLoader/host.d.ts +1 -1
  50. package/dist/overrides/imageLoader/host.js +1 -1
  51. package/dist/overrides/imageLoader/s3-lite.d.ts +1 -1
  52. package/dist/overrides/imageLoader/s3-lite.js +1 -1
  53. package/dist/overrides/imageLoader/s3.d.ts +1 -1
  54. package/dist/overrides/imageLoader/s3.js +1 -1
  55. package/dist/overrides/incrementalCache/dummy.d.ts +1 -1
  56. package/dist/overrides/incrementalCache/dummy.js +1 -1
  57. package/dist/overrides/incrementalCache/fs-dev.js +1 -1
  58. package/dist/overrides/incrementalCache/multi-tier-ddb-s3.d.ts +1 -1
  59. package/dist/overrides/incrementalCache/multi-tier-ddb-s3.js +2 -2
  60. package/dist/overrides/incrementalCache/s3-lite.d.ts +1 -1
  61. package/dist/overrides/incrementalCache/s3-lite.js +2 -2
  62. package/dist/overrides/incrementalCache/s3.d.ts +1 -1
  63. package/dist/overrides/originResolver/dummy.d.ts +1 -1
  64. package/dist/overrides/originResolver/pattern-env.d.ts +1 -1
  65. package/dist/overrides/proxyExternalRequest/dummy.d.ts +1 -1
  66. package/dist/overrides/proxyExternalRequest/dummy.js +1 -1
  67. package/dist/overrides/proxyExternalRequest/fetch.d.ts +1 -1
  68. package/dist/overrides/proxyExternalRequest/fetch.js +1 -1
  69. package/dist/overrides/proxyExternalRequest/node.d.ts +1 -1
  70. package/dist/overrides/queue/dummy.d.ts +1 -1
  71. package/dist/overrides/queue/dummy.js +1 -1
  72. package/dist/overrides/queue/sqs-lite.d.ts +1 -1
  73. package/dist/overrides/queue/sqs-lite.js +2 -2
  74. package/dist/overrides/queue/sqs.d.ts +1 -1
  75. package/dist/overrides/tagCache/dummy.d.ts +1 -1
  76. package/dist/overrides/tagCache/dynamodb-lite.d.ts +1 -1
  77. package/dist/overrides/tagCache/dynamodb-lite.js +2 -2
  78. package/dist/overrides/tagCache/dynamodb-nextMode.d.ts +1 -1
  79. package/dist/overrides/tagCache/dynamodb-nextMode.js +2 -2
  80. package/dist/overrides/tagCache/dynamodb.d.ts +1 -1
  81. package/dist/overrides/tagCache/fs-dev-nextMode.d.ts +1 -1
  82. package/dist/overrides/tagCache/fs-dev.d.ts +1 -1
  83. package/dist/overrides/tagCache/fs-dev.js +1 -1
  84. package/dist/overrides/warmer/aws-lambda.d.ts +1 -1
  85. package/dist/overrides/warmer/dummy.d.ts +1 -1
  86. package/dist/overrides/warmer/dummy.js +1 -1
  87. package/dist/overrides/wrappers/aws-apigw-streaming.d.ts +1 -1
  88. package/dist/overrides/wrappers/aws-lambda-compressed.d.ts +1 -1
  89. package/dist/overrides/wrappers/aws-lambda-streaming.d.ts +1 -1
  90. package/dist/overrides/wrappers/aws-lambda.d.ts +1 -1
  91. package/dist/overrides/wrappers/cloudflare-edge.d.ts +2 -2
  92. package/dist/overrides/wrappers/cloudflare-node.d.ts +2 -2
  93. package/dist/overrides/wrappers/dummy.d.ts +1 -1
  94. package/dist/overrides/wrappers/express-dev.js +2 -2
  95. package/dist/overrides/wrappers/node.d.ts +1 -1
  96. package/dist/plugins/resolve.d.ts +2 -2
  97. package/dist/types/global.d.ts +1 -1
  98. package/dist/types/overrides.d.ts +1 -1
  99. package/dist/utils/cache.d.ts +1 -1
  100. package/dist/utils/promise.d.ts +1 -1
  101. package/package.json +12 -12
@@ -1,26 +1,90 @@
1
- import { NextConfig } from "../../adapters/config/index";
2
- import { compile, match } from "path-to-regexp";
3
- import { normalizeRepeatedSlashes } from "../../utils/normalize-path";
4
- import { emptyReadableStream, toReadableStream } from "../../utils/stream";
1
+ import { NextConfig } from "../../adapters/config/index.js";
2
+ import { compile, match, parse } from "path-to-regexp";
3
+ import { normalizeRepeatedSlashes } from "../../utils/normalize-path.js";
4
+ import { emptyReadableStream, toReadableStream } from "../../utils/stream.js";
5
5
  import { debug } from "../../adapters/logger";
6
6
  import { handleLocaleRedirect, localizePath } from "./i18n";
7
7
  import { dynamicRouteMatcher, staticRouteMatcher } from "./routeMatcher";
8
8
  import { constructNextUrl, convertFromQueryString, convertToQueryString, escapeRegex, getUrlParts, isExternal, unescapeRegex, } from "./util";
9
+ const compiledRegExps = new Map();
10
+ /**
11
+ * Returns the compiled regular expression for a configured pattern.
12
+ *
13
+ * Patterns come from the Next.js config and manifests, so each one is compiled
14
+ * on first use and reused by later requests. `lastIndex` is reset on every call
15
+ * so a shared global or sticky expression always starts from the beginning.
16
+ *
17
+ * @param pattern The regular expression source
18
+ * @param flags The optional regular expression flags
19
+ * @returns The compiled regular expression
20
+ * @throws {SyntaxError} When the pattern is invalid
21
+ */
22
+ export function getCompiledRegExp(pattern, flags = "") {
23
+ const key = `/${pattern}/${flags}`;
24
+ let regExp = compiledRegExps.get(key);
25
+ if (!regExp) {
26
+ regExp = new RegExp(pattern, flags);
27
+ compiledRegExps.set(key, regExp);
28
+ }
29
+ regExp.lastIndex = 0;
30
+ return regExp;
31
+ }
32
+ /**
33
+ * Tests a request value against a route condition.
34
+ *
35
+ * Repeated query parameters are present when their array exists, but configured
36
+ * patterns match only the final entry, including an empty final entry. See
37
+ * https://github.com/vercel/next.js/blob/ae745ba/packages/next/src/shared/lib/router/utils/prepare-destination.ts#L92-L115
38
+ *
39
+ * @param value The scalar or repeated request value
40
+ * @param pattern The optional configured condition pattern
41
+ * @returns Whether the request value satisfies the condition
42
+ * @throws {SyntaxError} When the configured pattern is invalid
43
+ */
44
+ function matchHasValue(value, pattern) {
45
+ if (!value)
46
+ return false;
47
+ if (!pattern)
48
+ return true;
49
+ const candidate = Array.isArray(value) ? value.at(-1) : value;
50
+ return (candidate !== undefined && getCompiledRegExp(`^${pattern}$`).test(candidate));
51
+ }
52
+ /**
53
+ * Normalizes the request host for route-condition matching and captures.
54
+ *
55
+ * Next.js removes the port and lowercases the hostname before applying host
56
+ * patterns. See
57
+ * https://github.com/vercel/next.js/blob/ae745ba/packages/next/src/shared/lib/router/utils/prepare-destination.ts#L82-L88
58
+ *
59
+ * @param headers The request headers
60
+ * @returns The normalized hostname, or undefined when no host is present
61
+ */
62
+ function getHostname(headers) {
63
+ return headers.host?.split(":", 1)[0].toLowerCase();
64
+ }
65
+ /**
66
+ * Removes characters unsupported by path-to-regexp parameter names.
67
+ *
68
+ * Next.js exposes value-less conditions under a name containing only ASCII
69
+ * letters. See
70
+ * https://github.com/vercel/next.js/blob/ae745ba/packages/next/src/shared/lib/router/utils/prepare-destination.ts#L21-L36
71
+ *
72
+ * @param name The route-condition key
73
+ * @returns The sanitized destination parameter name
74
+ */
75
+ function getSafeParamName(name) {
76
+ return name.replaceAll(/[^a-zA-Z]/g, "");
77
+ }
9
78
  const routeHasMatcher = (headers, cookies, query) => (redirect) => {
10
79
  switch (redirect.type) {
11
80
  case "header":
12
- return (!!headers?.[redirect.key.toLowerCase()] &&
13
- new RegExp(redirect.value ?? "").test(headers[redirect.key.toLowerCase()] ?? ""));
81
+ return matchHasValue(headers[redirect.key.toLowerCase()], redirect.value);
14
82
  case "cookie":
15
- return (!!cookies?.[redirect.key] &&
16
- new RegExp(redirect.value ?? "").test(cookies[redirect.key] ?? ""));
83
+ return matchHasValue(cookies[redirect.key], redirect.value);
17
84
  case "query":
18
- return query[redirect.key] && Array.isArray(redirect.value)
19
- ? redirect.value.reduce((prev, current) => prev || new RegExp(current).test(query[redirect.key]), false)
20
- : new RegExp(redirect.value ?? "").test(query[redirect.key] ?? "");
85
+ return matchHasValue(query[redirect.key], redirect.value);
21
86
  case "host":
22
- return (headers?.host !== "" &&
23
- new RegExp(redirect.value ?? "").test(headers.host));
87
+ return matchHasValue(getHostname(headers), redirect.value);
24
88
  default:
25
89
  return false;
26
90
  }
@@ -39,13 +103,48 @@ const getParamsFromSource = (source) => (value) => {
39
103
  const _match = source(value);
40
104
  return _match ? _match.params : {};
41
105
  };
106
+ /**
107
+ * Creates an extractor for route-condition captures.
108
+ *
109
+ * Value-less conditions expose their present value under a safe parameter
110
+ * name. Patterned repeated query parameters capture their final value, matching
111
+ * the value used to decide the condition. See
112
+ * https://github.com/vercel/next.js/blob/ae745ba/packages/next/src/shared/lib/router/utils/prepare-destination.ts#L92-L107
113
+ *
114
+ * @param headers The request headers
115
+ * @param cookies The parsed request cookies
116
+ * @param query The request query parameters
117
+ * @returns A function that extracts named captures for one condition
118
+ * @throws {SyntaxError} When the configured pattern is invalid
119
+ */
42
120
  const computeParamHas = (headers, cookies, query) => (has) => {
43
- if (!has.value)
44
- return {};
45
- const matcher = new RegExp(`^${has.value}$`);
46
- const fromSource = (value) => {
121
+ if (!has.value) {
122
+ let key;
123
+ let value;
124
+ switch (has.type) {
125
+ case "header":
126
+ key = has.key.toLowerCase();
127
+ value = headers[key];
128
+ break;
129
+ case "cookie":
130
+ key = has.key;
131
+ value = cookies[key];
132
+ break;
133
+ case "query":
134
+ key = has.key;
135
+ value = query[key];
136
+ break;
137
+ case "host":
138
+ return {};
139
+ }
140
+ return value ? { [getSafeParamName(key)]: value } : {};
141
+ }
142
+ const matcher = getCompiledRegExp(`^${has.value}$`);
143
+ const fromSource = (value, implicitKey) => {
47
144
  const matches = value.match(matcher);
48
- return matches?.groups ?? {};
145
+ if (matches?.groups)
146
+ return matches.groups;
147
+ return implicitKey && matches?.[0] ? { [implicitKey]: matches[0] } : {};
49
148
  };
50
149
  switch (has.type) {
51
150
  case "header":
@@ -53,39 +152,108 @@ const computeParamHas = (headers, cookies, query) => (has) => {
53
152
  case "cookie":
54
153
  return fromSource(cookies[has.key] ?? "");
55
154
  case "query":
56
- return Array.isArray(query[has.key])
57
- ? fromSource(query[has.key].join(","))
58
- : fromSource(query[has.key] ?? "");
155
+ return fromSource(Array.isArray(query[has.key])
156
+ ? (query[has.key].at(-1) ?? "")
157
+ : (query[has.key] ?? ""));
59
158
  case "host":
60
- return fromSource(headers.host ?? "");
159
+ return fromSource(getHostname(headers) ?? "", "host");
61
160
  }
62
161
  };
63
- function convertMatch(match, toDestination, destination) {
64
- if (!match) {
65
- return destination;
162
+ /**
163
+ * Compiles parameters in a non-path value while preserving literal syntax.
164
+ *
165
+ * Header keys and values can contain URL schemes and characters that
166
+ * path-to-regexp otherwise treats as patterns. See
167
+ * https://github.com/vercel/next.js/blob/ae745ba/packages/next/src/shared/lib/router/utils/prepare-destination.ts#L127-L160
168
+ *
169
+ * @param value The configured header key or value
170
+ * @param params The source and route-condition parameters
171
+ * @returns The interpolated value
172
+ * @throws {TypeError} When a referenced parameter cannot be compiled
173
+ */
174
+ function compileNonPath(value, params) {
175
+ if (!value.includes(":"))
176
+ return value;
177
+ let compiledValue = value;
178
+ for (const key of Object.keys(params)) {
179
+ if (!compiledValue.includes(`:${key}`))
180
+ continue;
181
+ const escapedKey = key.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
182
+ compiledValue = compiledValue
183
+ .replace(getCompiledRegExp(`:${escapedKey}\\*`, "g"), `:${key}--ESCAPED_PARAM_ASTERISKS`)
184
+ .replace(getCompiledRegExp(`:${escapedKey}\\?`, "g"), `:${key}--ESCAPED_PARAM_QUESTION`)
185
+ .replace(getCompiledRegExp(`:${escapedKey}\\+`, "g"), `:${key}--ESCAPED_PARAM_PLUS`)
186
+ .replace(getCompiledRegExp(`:${escapedKey}(?!\\w)`, "g"), `--ESCAPED_PARAM_COLON${key}`);
66
187
  }
67
- const { params } = match;
68
- const isUsingParams = Object.keys(params).length > 0;
69
- return isUsingParams ? toDestination(params) : destination;
188
+ compiledValue = compiledValue
189
+ .replace(/(:|\*|\?|\+|\(|\)|\{|\})/g, "\\$1")
190
+ .replaceAll("--ESCAPED_PARAM_PLUS", "+")
191
+ .replaceAll("--ESCAPED_PARAM_COLON", ":")
192
+ .replaceAll("--ESCAPED_PARAM_QUESTION", "?")
193
+ .replaceAll("--ESCAPED_PARAM_ASTERISKS", "*");
194
+ return compile(`/${compiledValue}`, { validate: false })(params).slice(1);
70
195
  }
196
+ /**
197
+ * Compiles each destination query value independently.
198
+ *
199
+ * Independent compilation gives repeat parameters the same slash separator as
200
+ * Next.js while preserving configured query keys and delimiters.
201
+ *
202
+ * @param queryString The destination query string without a leading question mark
203
+ * @param params The source and route-condition parameters
204
+ * @returns The interpolated destination query string
205
+ * @throws {TypeError} When a referenced parameter cannot be compiled
206
+ */
207
+ function compileQueryString(queryString, params) {
208
+ return queryString
209
+ .split("&")
210
+ .map((part) => {
211
+ const separator = part.indexOf("=");
212
+ if (separator === -1)
213
+ return part;
214
+ const key = part.slice(0, separator + 1);
215
+ return `${key}${compileNonPath(part.slice(separator + 1), params)}`;
216
+ })
217
+ .join("&");
218
+ }
219
+ /**
220
+ * Resolves configured response headers for a request.
221
+ *
222
+ * Source parameters are merged with successful condition parameters before
223
+ * interpolation, with condition parameters taking precedence. See
224
+ * https://github.com/vercel/next.js/blob/ae745ba/packages/next/src/server/lib/router-utils/resolve-routes.ts#L400-L414
225
+ *
226
+ * @param event The request used to match configured headers
227
+ * @param configHeaders The configured header routes
228
+ * @returns The response headers produced by every matching route
229
+ * @throws {SyntaxError} When a configured route pattern is invalid
230
+ */
71
231
  export function getNextConfigHeaders(event, configHeaders) {
72
232
  if (!configHeaders) {
73
233
  return {};
74
234
  }
75
235
  const matcher = routeHasMatcher(event.headers, event.cookies, event.query);
236
+ const computeHas = computeParamHas(event.headers, event.cookies, event.query);
76
237
  const requestHeaders = {};
77
238
  const localizedRawPath = localizePath(event);
78
239
  for (const { headers, has, missing, regex, source, locale, } of configHeaders) {
79
240
  const path = locale === false ? event.rawPath : localizedRawPath;
80
- if (new RegExp(regex).test(path) &&
241
+ if (getCompiledRegExp(regex).test(path) &&
81
242
  checkHas(matcher, has) &&
82
243
  checkHas(matcher, missing, true)) {
83
244
  const fromSource = match(source);
84
245
  const _match = fromSource(path);
246
+ const params = {
247
+ ...(_match ? _match.params : {}),
248
+ ...has?.reduce((acc, cur) => {
249
+ return Object.assign(acc, computeHas(cur));
250
+ }, {}),
251
+ };
252
+ const hasParams = Object.keys(params).length > 0;
85
253
  headers.forEach((h) => {
86
254
  try {
87
- const key = convertMatch(_match, compile(h.key), h.key);
88
- const value = convertMatch(_match, compile(h.value), h.value);
255
+ const key = hasParams ? compileNonPath(h.key, params) : h.key;
256
+ const value = hasParams ? compileNonPath(h.value, params) : h.value;
89
257
  requestHeaders[key] = value;
90
258
  }
91
259
  catch {
@@ -98,18 +266,34 @@ export function getNextConfigHeaders(event, configHeaders) {
98
266
  return requestHeaders;
99
267
  }
100
268
  /**
101
- * TODO: This method currently only check for the first match.
102
- * It should check for all matches for `beforeFiles` and `afterFiles` rewrite
103
- * See https://nextjs.org/docs/app/api-reference/config/next-config-js/rewrites
269
+ * Applies the first matching rewrite to an internal request.
270
+ *
271
+ * Only successful `has` predicates supply destination parameters. Nonmatching
272
+ * `missing` predicates must not manufacture captures from absent values. See
273
+ * https://github.com/vercel/next.js/blob/ae745ba/packages/next/src/shared/lib/router/utils/prepare-destination.ts#L113-L125
274
+ *
275
+ * Destination queries use non-path compilation so repeated parameters retain
276
+ * their separators. See
277
+ * https://github.com/vercel/next.js/blob/ae745ba/packages/next/src/shared/lib/router/utils/prepare-destination.ts#L292-L302
278
+ *
279
+ * TODO: This method currently only checks the first match. It should check all
280
+ * matches for `beforeFiles` and `afterFiles` rewrites. See
281
+ * https://nextjs.org/docs/app/api-reference/config/next-config-js/rewrites
282
+ *
283
+ * @param event The request to rewrite
284
+ * @param rewrites The configured rewrites to evaluate
285
+ * @param appendParamsToQuery Whether unused match parameters are added to the query
286
+ * @returns The rewritten request and matched rewrite metadata
287
+ * @throws {TypeError} When a matched destination cannot be compiled
104
288
  */
105
- export function handleRewrites(event, rewrites) {
289
+ export function handleRewrites(event, rewrites, appendParamsToQuery = true) {
106
290
  const { rawPath, headers, query, cookies, url } = event;
107
291
  const localizedRawPath = localizePath(event);
108
292
  const matcher = routeHasMatcher(headers, cookies, query);
109
293
  const computeHas = computeParamHas(headers, cookies, query);
110
294
  const rewrite = rewrites.find((route) => {
111
295
  const path = route.locale === false ? rawPath : localizedRawPath;
112
- return (new RegExp(route.regex).test(path) &&
296
+ return (getCompiledRegExp(route.regex).test(path) &&
113
297
  checkHas(matcher, route.has) &&
114
298
  checkHas(matcher, route.missing, true));
115
299
  });
@@ -118,14 +302,16 @@ export function handleRewrites(event, rewrites) {
118
302
  const isExternalRewrite = isExternal(rewrite?.destination);
119
303
  debug("isExternalRewrite", isExternalRewrite);
120
304
  if (rewrite) {
121
- const { pathname, protocol, hostname, queryString } = getUrlParts(rewrite.destination, isExternalRewrite);
305
+ const { pathname, protocol, hostname, queryString, hash } = getUrlParts(rewrite.destination, isExternalRewrite);
122
306
  // We need to use a localized path if the rewrite is not locale specific
123
307
  const pathToUse = rewrite.locale === false ? rawPath : localizedRawPath;
124
- debug("urlParts", { pathname, protocol, hostname, queryString });
125
- const toDestinationPath = compile(escapeRegex(pathname, { isPath: true }));
308
+ debug("urlParts", { pathname, protocol, hostname, queryString, hash });
309
+ // Values were validated while matching; Next.js does not revalidate them
310
+ // as single path segments. https://github.com/vercel/next.js/blob/ae745ba/packages/next/src/shared/lib/router/utils/prepare-destination.ts#L254-L268
311
+ const compileOptions = { validate: false };
312
+ const toDestinationPath = compile(escapeRegex(pathname, { isPath: true }), compileOptions);
126
313
  // A literal numeric port is URL syntax, not a path-to-regexp parameter.
127
- const toDestinationHost = compile(escapeRegex(hostname).replace(/:(\d+)$/, "\\:$1"));
128
- const toDestinationQuery = compile(escapeRegex(queryString));
314
+ const toDestinationHost = compile(escapeRegex(hostname).replace(/:(\d+)$/, "\\:$1"), { ...compileOptions, encode: encodeURIComponent });
129
315
  const params = {
130
316
  // params for the source
131
317
  ...getParamsFromSource(match(escapeRegex(rewrite.source, { isPath: true })))(pathToUse),
@@ -133,23 +319,33 @@ export function handleRewrites(event, rewrites) {
133
319
  ...rewrite.has?.reduce((acc, cur) => {
134
320
  return Object.assign(acc, computeHas(cur));
135
321
  }, {}),
136
- // params for the missing
137
- ...rewrite.missing?.reduce((acc, cur) => {
138
- return Object.assign(acc, computeHas(cur));
139
- }, {}),
140
322
  };
141
323
  const isUsingParams = Object.keys(params).length > 0;
324
+ const queryParams = Object.fromEntries(Object.entries(params).filter(([key]) => key !== "nextInternalLocale"));
325
+ // Next.js appends match parameters only when none are consumed by the
326
+ // destination path or hostname. See
327
+ // https://github.com/vercel/next.js/blob/ae745ba/packages/next/src/shared/lib/router/utils/prepare-destination.ts#L286-L302
328
+ const destinationParams = [
329
+ ...parse(escapeRegex(`${pathname}${hash}`, { isPath: true })),
330
+ ...(hostname
331
+ ? parse(escapeRegex(hostname).replace(/:(\d+)$/, "\\:$1"))
332
+ : []),
333
+ ].flatMap((token) => typeof token === "string" ? [] : [String(token.name)]);
142
334
  let rewrittenQuery = queryString;
143
335
  let rewrittenHost = hostname;
144
- let rewrittenPath = pathname;
336
+ let rewrittenHash = hash;
337
+ let rewrittenPath = unescapeRegex(toDestinationPath(params));
338
+ if (pathname.startsWith("/") && !rewrittenPath.startsWith("/")) {
339
+ rewrittenPath = `/${rewrittenPath}`;
340
+ }
145
341
  if (isUsingParams) {
146
- rewrittenPath = unescapeRegex(toDestinationPath(params));
147
342
  rewrittenHost = unescapeRegex(toDestinationHost(params));
148
- rewrittenQuery = unescapeRegex(toDestinationQuery(params));
343
+ rewrittenQuery = compileQueryString(queryString, params);
344
+ rewrittenHash = compileNonPath(hash, params);
149
345
  }
150
346
  // We need to strip the locale from the path if it's a local api route
151
347
  if (NextConfig.i18n && !isExternalRewrite) {
152
- const strippedPathLocale = rewrittenPath.replace(new RegExp(`^/(${NextConfig.i18n.locales.join("|")})`), "");
348
+ const strippedPathLocale = rewrittenPath.replace(getCompiledRegExp(`^/(${NextConfig.i18n.locales.join("|")})`), "");
153
349
  if (strippedPathLocale.startsWith("/api/")) {
154
350
  rewrittenPath = strippedPathLocale;
155
351
  }
@@ -158,11 +354,17 @@ export function handleRewrites(event, rewrites) {
158
354
  ? `${protocol}//${rewrittenHost}${rewrittenPath}`
159
355
  : new URL(rewrittenPath, event.url).href;
160
356
  // We merge query params from the source and the destination
357
+ const destinationQuery = convertFromQueryString(rewrittenQuery);
161
358
  finalQuery = {
162
359
  ...query,
163
- ...convertFromQueryString(rewrittenQuery),
360
+ ...(!appendParamsToQuery ||
361
+ Object.keys(queryParams).some((key) => destinationParams.includes(key))
362
+ ? {}
363
+ : queryParams),
364
+ ...destinationQuery,
164
365
  };
165
366
  rewrittenUrl += convertToQueryString(finalQuery);
367
+ rewrittenUrl += rewrittenHash;
166
368
  debug("rewrittenUrl", { rewrittenUrl, finalQuery, isUsingParams });
167
369
  }
168
370
  return {
@@ -176,6 +378,35 @@ export function handleRewrites(event, rewrites) {
176
378
  isExternalRewrite,
177
379
  };
178
380
  }
381
+ /**
382
+ * Applies every matching `beforeFiles` rewrite, in order.
383
+ *
384
+ * Next.js keeps evaluating `beforeFiles` rewrites after a match, each one
385
+ * against the URL produced by the previous match. An external rewrite ends the
386
+ * evaluation. See
387
+ * https://github.com/vercel/next.js/blob/ae745ba/packages/next/src/server/lib/router-utils/resolve-routes.ts#L863-L932
388
+ *
389
+ * @param event The request to rewrite
390
+ * @param rewrites The configured `beforeFiles` rewrites
391
+ * @returns The rewritten request and the last matched rewrite metadata
392
+ * @throws {TypeError} When a matched destination cannot be compiled
393
+ */
394
+ export function handleBeforeFilesRewrites(event, rewrites) {
395
+ let result = {
396
+ internalEvent: event,
397
+ __rewrite: undefined,
398
+ isExternalRewrite: false,
399
+ };
400
+ for (const rewrite of rewrites) {
401
+ const rewriteResult = handleRewrites(result.internalEvent, [rewrite]);
402
+ if (!rewriteResult.__rewrite)
403
+ continue;
404
+ result = rewriteResult;
405
+ if (result.isExternalRewrite)
406
+ break;
407
+ }
408
+ return result;
409
+ }
179
410
  // Normalizes repeated slashes in the path e.g. hello//world -> hello/world
180
411
  // or backslashes to forward slashes. This prevents requests such as //domain
181
412
  // from invoking the middleware with `request.url === "domain"`.
@@ -250,7 +481,7 @@ export function handleRedirects(event, redirects) {
250
481
  const localeRedirect = handleLocaleRedirect(event);
251
482
  if (localeRedirect)
252
483
  return localeRedirect;
253
- const { internalEvent, __rewrite } = handleRewrites(event, redirects.filter((r) => !r.internal));
484
+ const { internalEvent, __rewrite } = handleRewrites(event, redirects.filter((r) => !r.internal), false);
254
485
  if (__rewrite && !__rewrite.internal) {
255
486
  return {
256
487
  type: event.type,
@@ -301,10 +532,7 @@ export function handleFallbackFalse(internalEvent, prerenderManifest) {
301
532
  const { rawPath } = internalEvent;
302
533
  const { dynamicRoutes = {}, routes = {} } = prerenderManifest ?? {};
303
534
  const prerenderedFallbackRoutes = Object.entries(dynamicRoutes).filter(([, { fallback }]) => fallback === false);
304
- const routeFallback = prerenderedFallbackRoutes.some(([, { routeRegex }]) => {
305
- const routeRegexExp = new RegExp(routeRegex);
306
- return routeRegexExp.test(rawPath);
307
- });
535
+ const routeFallback = prerenderedFallbackRoutes.some(([, { routeRegex }]) => getCompiledRegExp(routeRegex).test(rawPath));
308
536
  const locales = NextConfig.i18n?.locales;
309
537
  const routesAlreadyHaveLocale = locales?.includes(rawPath.split("/")[1]) ||
310
538
  // If we don't use locales, we don't need to add the default locale
@@ -4,11 +4,15 @@ type MiddlewareLoader = () => Promise<{
4
4
  default: Middleware;
5
5
  }>;
6
6
  /**
7
+ * Runs matching Next.js middleware and applies its routing result.
7
8
  *
8
- * @param internalEvent the internal event
9
- * @param initialSearch the initial query string as it was received in the handler
10
- * @param middlewareLoader Only used for unit test
11
- * @returns `Promise<MiddlewareEvent | InternalResult>`
9
+ * Middleware request-header overrides are normalized and filtered to Next.js's allowlist.
10
+ *
11
+ * @param internalEvent The internal request event.
12
+ * @param initialSearch The initial query string received by the handler.
13
+ * @param middlewareLoader The middleware loader, overridden by unit tests.
14
+ * @return The middleware-adjusted event or a terminal middleware response.
15
+ * @throws When middleware loading, execution, or response processing fails.
12
16
  */
13
17
  export declare function handleMiddleware(internalEvent: InternalEvent, initialSearch: string, middlewareLoader?: MiddlewareLoader): Promise<MiddlewareEvent | InternalResult>;
14
18
  export {};
@@ -14,11 +14,15 @@ function defaultMiddlewareLoader() {
14
14
  return import("./middleware.mjs");
15
15
  }
16
16
  /**
17
+ * Runs matching Next.js middleware and applies its routing result.
17
18
  *
18
- * @param internalEvent the internal event
19
- * @param initialSearch the initial query string as it was received in the handler
20
- * @param middlewareLoader Only used for unit test
21
- * @returns `Promise<MiddlewareEvent | InternalResult>`
19
+ * Middleware request-header overrides are normalized and filtered to Next.js's allowlist.
20
+ *
21
+ * @param internalEvent The internal request event.
22
+ * @param initialSearch The initial query string received by the handler.
23
+ * @param middlewareLoader The middleware loader, overridden by unit tests.
24
+ * @return The middleware-adjusted event or a terminal middleware response.
25
+ * @throws When middleware loading, execution, or response processing fails.
22
26
  */
23
27
  export async function handleMiddleware(internalEvent, initialSearch, middlewareLoader = defaultMiddlewareLoader) {
24
28
  const headers = internalEvent.headers;
@@ -81,11 +85,15 @@ export async function handleMiddleware(internalEvent, initialSearch, middlewareL
81
85
 
82
86
  We can delete `x-middleware-override-headers` and check if the key starts with
83
87
  x-middleware-request- to set the req headers
88
+
89
+ The request headers that are not listed in `x-middleware-override-headers` have been
90
+ deleted by the middleware and must not be forwarded.
84
91
  */
85
92
  const responseHeaders = result.headers;
86
93
  const reqHeaders = {};
87
94
  const resHeaders = {};
88
95
  // These are internal headers used by Next.js, we don't want to expose them to the client
96
+ // NOTE: keys must be in lowercase to match the normalized header keys.
89
97
  const filteredHeaders = [
90
98
  "x-middleware-override-headers",
91
99
  "x-middleware-next",
@@ -95,21 +103,23 @@ export async function handleMiddleware(internalEvent, initialSearch, middlewareL
95
103
  ];
96
104
  const xMiddlewareKey = "x-middleware-request-";
97
105
  responseHeaders.forEach((value, key) => {
98
- if (key.startsWith(xMiddlewareKey)) {
99
- const k = key.substring(xMiddlewareKey.length);
106
+ // Always use the normalized key (lowercase) for comparison and storage.
107
+ const normalizedKey = key.toLowerCase();
108
+ if (normalizedKey.startsWith(xMiddlewareKey)) {
109
+ const k = normalizedKey.substring(xMiddlewareKey.length);
100
110
  reqHeaders[k] = value;
101
111
  }
102
112
  else {
103
- if (filteredHeaders.includes(key.toLowerCase()))
113
+ if (filteredHeaders.includes(normalizedKey))
104
114
  return;
105
115
  // Headers.forEach folds same-name headers; use getSetCookie() below instead.
106
- if (key.toLowerCase() === "set-cookie")
116
+ if (normalizedKey === "set-cookie")
107
117
  return;
108
- if (REDIRECTS.has(statusCode) && key.toLowerCase() === "location") {
109
- resHeaders[key] = normalizeLocationHeader(value, internalEvent.url);
118
+ if (REDIRECTS.has(statusCode) && normalizedKey === "location") {
119
+ resHeaders[normalizedKey] = normalizeLocationHeader(value, internalEvent.url);
110
120
  }
111
121
  else {
112
- resHeaders[key] = value;
122
+ resHeaders[normalizedKey] = value;
113
123
  }
114
124
  }
115
125
  });
@@ -122,6 +132,13 @@ export async function handleMiddleware(internalEvent, initialSearch, middlewareL
122
132
  if (setCookies.length > 0) {
123
133
  resHeaders["set-cookie"] = setCookies;
124
134
  }
135
+ const overrideHeaders = responseHeaders.get("x-middleware-override-headers");
136
+ const overriddenHeaders = overrideHeaders === null
137
+ ? undefined
138
+ : new Set(overrideHeaders.split(",").map((key) => key.trim().toLowerCase()));
139
+ const newHeaders = Object.fromEntries([...Object.entries(internalEvent.headers), ...Object.entries(reqHeaders)]
140
+ .map(([key, value]) => [key.toLowerCase(), value])
141
+ .filter(([key]) => !overriddenHeaders || overriddenHeaders.has(key)));
125
142
  // If the middleware returned a Rewrite, set the `url` to the pathname of the rewrite
126
143
  // NOTE: the header was added to `req` from above
127
144
  const rewriteUrl = responseHeaders.get("x-middleware-rewrite");
@@ -162,7 +179,7 @@ export async function handleMiddleware(internalEvent, initialSearch, middlewareL
162
179
  url: newUrl,
163
180
  rawPath: new URL(newUrl).pathname,
164
181
  type: internalEvent.type,
165
- headers: { ...internalEvent.headers, ...reqHeaders },
182
+ headers: newHeaders,
166
183
  body: internalEvent.body,
167
184
  method: internalEvent.method,
168
185
  query: middlewareQuery,
@@ -1,3 +1,3 @@
1
- import type { ResolvedRoute } from "../../types/open-next";
1
+ import type { ResolvedRoute } from "../../types/open-next.js";
2
2
  export declare const staticRouteMatcher: (path: string) => ResolvedRoute[];
3
3
  export declare const dynamicRouteMatcher: (path: string) => ResolvedRoute[];
@@ -1,4 +1,4 @@
1
- import { AppPathRoutesManifest, PagesManifest, RoutesManifest, } from "../../adapters/config/index";
1
+ import { AppPathRoutesManifest, PagesManifest, RoutesManifest, } from "../../adapters/config/index.js";
2
2
  // Add the locale prefix to the regex so we correctly match the rawPath
3
3
  const optionalLocalePrefixRegex = `^/(?:${RoutesManifest.locales.map((locale) => `${locale}/?`).join("|")})?`;
4
4
  // Add the basepath prefix to the regex so we correctly match the rawPath
@@ -1,7 +1,7 @@
1
1
  import type { OutgoingHttpHeaders } from "node:http";
2
2
  import type { IncomingMessage } from "../../http/index.js";
3
3
  import { OpenNextNodeResponse } from "../../http/openNextResponse.js";
4
- import type { FunctionsConfigManifest, MiddlewareManifest } from "../../types/next-types";
4
+ import type { FunctionsConfigManifest, MiddlewareManifest } from "../../types/next-types.js";
5
5
  import type { InternalEvent, InternalResult, RoutingResult, StreamCreator } from "../../types/open-next.js";
6
6
  import { ReadableStream } from "node:stream/web";
7
7
  /**
@@ -9,17 +9,31 @@ import { ReadableStream } from "node:stream/web";
9
9
  * @__PURE__
10
10
  */
11
11
  export declare function isExternal(url?: string, host?: string): boolean;
12
+ /**
13
+ * Parses a raw query string into decoded scalar or repeated values.
14
+ *
15
+ * The result has the same shape the converters produce from `URLSearchParams`
16
+ * and follows Next.js's decoded query representation.
17
+ * https://github.com/vercel/next.js/blob/3439bde/packages/next/src/shared/lib/router/utils/querystring.ts#L34-L48
18
+ *
19
+ * @param query The query string without a leading question mark
20
+ * @returns The decoded scalar and repeated query values
21
+ *
22
+ * @__PURE__
23
+ */
12
24
  export declare function convertFromQueryString(query: string): Record<string, string | string[]>;
13
25
  /**
14
26
  * Splits an URL into its parts
15
27
  *
16
28
  * The URL is a route destination, i.e. a `path-to-regexp` pattern rather than a
17
29
  * well formed URL, so it can not be parsed with `new URL()` which would percent
18
- * encode some of the pattern characters.
30
+ * encode some of the pattern characters. Like Next.js, the fragment is kept
31
+ * separate from the query while destination parameters are interpolated.
32
+ * https://github.com/vercel/next.js/blob/3439bde/packages/next/src/shared/lib/router/utils/prepare-destination.ts#L221-L299
19
33
  *
20
34
  * @param url The URL to split
21
35
  * @param isExternal Whether the URL points to an external host
22
- * @returns The protocol, hostname, pathname and query string of the URL
36
+ * @returns The protocol, hostname, pathname, query string and fragment
23
37
  * @throws When `isExternal` is true and the URL is not an absolute HTTP(S) URL
24
38
  *
25
39
  * @__PURE__
@@ -29,6 +43,7 @@ export declare function getUrlParts(url: string, isExternal: boolean): {
29
43
  pathname: string;
30
44
  protocol: string;
31
45
  queryString: string;
46
+ hash: string;
32
47
  };
33
48
  /**
34
49
  * Creates an URL to a Next page
@@ -49,7 +64,8 @@ export declare function convertRes(res: OpenNextNodeResponse): InternalResult;
49
64
  * Make sure that multi-value query parameters are transformed to
50
65
  * ?key=value1&key=value2&... so that Next converts those parameters
51
66
  * to an array when reading the query parameters
52
- * query should be properly encoded before using this function
67
+ * Keys and values are expected decoded (as produced by the converters) and are
68
+ * percent-encoded here, so a value such as "h&m" survives as "h%26m"
53
69
  * @__PURE__
54
70
  */
55
71
  export declare function convertToQueryString(query: Record<string, string | string[]>): string;