@opennextjs/aws 4.1.14 → 4.1.16
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.
- package/dist/adapters/cache.js +2 -1
- package/dist/adapters/composable-cache.d.ts +20 -0
- package/dist/adapters/composable-cache.js +33 -8
- package/dist/adapters/edge-adapter.js +2 -11
- package/dist/core/requestHandler.d.ts +17 -0
- package/dist/core/requestHandler.js +48 -18
- package/dist/core/revalidate.js +1 -0
- package/dist/core/routing/cacheInterceptor.js +28 -24
- package/dist/core/routing/matcher.d.ts +14 -1
- package/dist/core/routing/matcher.js +41 -9
- package/dist/core/routing/middleware.d.ts +4 -0
- package/dist/core/routing/middleware.js +22 -0
- package/dist/core/routing/routeParamPlaceholder.d.ts +14 -0
- package/dist/core/routing/routeParamPlaceholder.js +42 -0
- package/dist/core/routing/util.js +10 -2
- package/dist/core/routingHandler.d.ts +2 -0
- package/dist/core/routingHandler.js +39 -29
- package/dist/http/framing.d.ts +35 -0
- package/dist/http/framing.js +181 -0
- package/dist/http/util.d.ts +11 -0
- package/dist/http/util.js +26 -0
- package/dist/minimize-js.d.ts +9 -0
- package/dist/minimize-js.js +27 -26
- package/dist/overrides/proxyExternalRequest/fetch.js +5 -1
- package/dist/overrides/proxyExternalRequest/node.js +5 -0
- package/dist/overrides/wrappers/express-dev.js +3 -5
- package/dist/overrides/wrappers/node.js +2 -3
- package/dist/types/global.d.ts +12 -0
- package/dist/utils/cache.d.ts +14 -0
- package/dist/utils/cache.js +20 -0
- package/dist/utils/normalize-path.d.ts +15 -0
- package/dist/utils/normalize-path.js +32 -0
- package/package.json +5 -5
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { dynamicRouteMatcher } from "./routeMatcher";
|
|
2
|
+
// The placeholders of a route pattern: `[slug]`, `[...slug]` or `[[...slug]]`
|
|
3
|
+
const PLACEHOLDER_REGEX = /\[\[\.\.\.[^\]]+\]\]|\[(?:\.\.\.)?[^\]]+\]/g;
|
|
4
|
+
// A placeholder starts with `[`, which is either literal or percent encoded in the pathname
|
|
5
|
+
const OPENING_BRACKET_REGEX = /\[|%5b/i;
|
|
6
|
+
/**
|
|
7
|
+
* Returns whether a segment of the pathname is the placeholder of a matched dynamic App Router page.
|
|
8
|
+
*
|
|
9
|
+
* Next.js versions before 16.4.5 treat a param whose value is its placeholder (e.g. `[slug]` for `[slug]`) as an
|
|
10
|
+
* unresolved fallback param. Such values are not legitimate and must not reach Next.js.
|
|
11
|
+
*
|
|
12
|
+
* Segments are decoded like Next.js decodes param values (catch-all values are split on `/` first).
|
|
13
|
+
* Segments with a malformed percent encoding never match.
|
|
14
|
+
* Pathnames without an opening bracket return early, without matching the dynamic routes.
|
|
15
|
+
*
|
|
16
|
+
* @param pathname The internal pathname, which may include the basePath, a locale prefix and a trailing slash
|
|
17
|
+
* @returns Whether a segment of the pathname is a placeholder of a matched dynamic App Router page
|
|
18
|
+
*/
|
|
19
|
+
export function hasRouteParamPlaceholder(pathname) {
|
|
20
|
+
if (!OPENING_BRACKET_REGEX.test(pathname)) {
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
const placeholders = new Set(dynamicRouteMatcher(pathname)
|
|
24
|
+
.filter(({ type }) => type === "app")
|
|
25
|
+
.flatMap(({ route }) => route.match(PLACEHOLDER_REGEX) ?? []));
|
|
26
|
+
return (placeholders.size > 0 &&
|
|
27
|
+
pathname.split("/").some((segment) => placeholders.has(safeDecode(segment))));
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Decodes a URI component without throwing.
|
|
31
|
+
*
|
|
32
|
+
* @param value The encoded value
|
|
33
|
+
* @returns The decoded value, or an empty string when the encoding is malformed
|
|
34
|
+
*/
|
|
35
|
+
function safeDecode(value) {
|
|
36
|
+
try {
|
|
37
|
+
return decodeURIComponent(value);
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
return "";
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -303,12 +303,20 @@ export async function revalidateIfRequired(host, rawPath, headers, req) {
|
|
|
303
303
|
// If data has the same etag during these 5 min dedup window, it will be deduplicated and not revalidated.
|
|
304
304
|
try {
|
|
305
305
|
const hash = (str) => crypto.createHash("md5").update(str).digest("hex");
|
|
306
|
-
const
|
|
306
|
+
const store = globalThis.__openNextAls.getStore();
|
|
307
|
+
// `1` when the entry is stale by tag, so that the deduplication id is the same for every copy of the entry
|
|
308
|
+
const lastModified = store?.lastModified ?? 0;
|
|
307
309
|
// For some weird cases, lastModified is not set, haven't been able to figure out yet why
|
|
308
310
|
// For those cases we add the etag to the deduplication id, it might help
|
|
309
311
|
const eTag = `${headers.etag ?? headers.ETag ?? ""}`;
|
|
310
312
|
await globalThis.queue.send({
|
|
311
|
-
MessageBody: {
|
|
313
|
+
MessageBody: {
|
|
314
|
+
host,
|
|
315
|
+
url: revalidateUrl,
|
|
316
|
+
eTag,
|
|
317
|
+
// The time of the entry, so that the queue can skip it when it was revalidated since
|
|
318
|
+
lastModified: store?.entryLastModified ?? lastModified,
|
|
319
|
+
},
|
|
312
320
|
MessageDeduplicationId: hash(`${rawPath}-${lastModified}-${eTag}`),
|
|
313
321
|
MessageGroupId: generateMessageGroupId(rawPath),
|
|
314
322
|
});
|
|
@@ -3,6 +3,8 @@ import type { AssetResolver } from "../types/overrides.js";
|
|
|
3
3
|
export declare const MIDDLEWARE_HEADER_PREFIX = "x-middleware-response-";
|
|
4
4
|
export declare const MIDDLEWARE_HEADER_PREFIX_LEN: number;
|
|
5
5
|
export declare const INTERNAL_HEADER_PREFIX = "x-opennext-";
|
|
6
|
+
export declare const NEXT_CONFIG_HEADER_PREFIX = "x-opennext-next-config-response-";
|
|
7
|
+
export declare const NEXT_CONFIG_HEADER_PREFIX_LEN: number;
|
|
6
8
|
export declare const INTERNAL_HEADER_INITIAL_URL = "x-opennext-initial-url";
|
|
7
9
|
export declare const INTERNAL_HEADER_LOCALE = "x-opennext-locale";
|
|
8
10
|
export declare const INTERNAL_HEADER_RESOLVED_ROUTES = "x-opennext-resolved-routes";
|
|
@@ -6,10 +6,13 @@ import { detectLocale } from "./routing/i18n";
|
|
|
6
6
|
import { fixDataPage, getNextConfigHeaders, handleBeforeFilesRewrites, handleFallbackFalse, handleRedirects, handleRewrites, } from "./routing/matcher";
|
|
7
7
|
import { handleMiddleware } from "./routing/middleware";
|
|
8
8
|
import { dynamicRouteMatcher, staticRouteMatcher, } from "./routing/routeMatcher";
|
|
9
|
+
import { hasRouteParamPlaceholder } from "./routing/routeParamPlaceholder";
|
|
9
10
|
import { constructNextUrl, normalizeLocationHeader } from "./routing/util";
|
|
10
11
|
export const MIDDLEWARE_HEADER_PREFIX = "x-middleware-response-";
|
|
11
12
|
export const MIDDLEWARE_HEADER_PREFIX_LEN = MIDDLEWARE_HEADER_PREFIX.length;
|
|
12
13
|
export const INTERNAL_HEADER_PREFIX = "x-opennext-";
|
|
14
|
+
export const NEXT_CONFIG_HEADER_PREFIX = `${INTERNAL_HEADER_PREFIX}next-config-response-`;
|
|
15
|
+
export const NEXT_CONFIG_HEADER_PREFIX_LEN = NEXT_CONFIG_HEADER_PREFIX.length;
|
|
13
16
|
export const INTERNAL_HEADER_INITIAL_URL = `${INTERNAL_HEADER_PREFIX}initial-url`;
|
|
14
17
|
export const INTERNAL_HEADER_LOCALE = `${INTERNAL_HEADER_PREFIX}locale`;
|
|
15
18
|
export const INTERNAL_HEADER_RESOLVED_ROUTES = `${INTERNAL_HEADER_PREFIX}resolved-routes`;
|
|
@@ -39,24 +42,34 @@ const NEXT_INTERNAL_HEADERS = [
|
|
|
39
42
|
"x-matched-path",
|
|
40
43
|
"x-nextjs-data",
|
|
41
44
|
"x-next-resume-state-length",
|
|
45
|
+
"next-resume",
|
|
42
46
|
];
|
|
43
47
|
/**
|
|
44
|
-
* Adds the middleware headers to an event or result.
|
|
48
|
+
* Adds the `next.config` and middleware response headers to an event or result.
|
|
45
49
|
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
50
|
+
* On an event, each kind gets its own prefix so that the request handler can tell them apart (see
|
|
51
|
+
* `splitRoutingHeaders`). On a result, the headers are the response and are not prefixed.
|
|
52
|
+
*
|
|
53
|
+
* `nextConfigHeaders` take precedence over `middlewareHeaders` on the response.
|
|
54
|
+
*
|
|
55
|
+
* @param eventOrResult The event or result to add the headers to, its headers are mutated
|
|
56
|
+
* @param nextConfigHeaders The response headers from the `next.config` `headers()`
|
|
57
|
+
* @param middlewareHeaders The response headers set by the middleware
|
|
48
58
|
*/
|
|
49
|
-
function
|
|
50
|
-
// Use the `MIDDLEWARE_HEADER_PREFIX` prefix for events, they will be processed by the request handler later.
|
|
51
|
-
// Results do not go through the request handler and should not be prefixed.
|
|
59
|
+
function applyResponseHeaders(eventOrResult, nextConfigHeaders, middlewareHeaders) {
|
|
52
60
|
const isResult = isInternalResult(eventOrResult);
|
|
53
61
|
const headers = eventOrResult.headers;
|
|
54
|
-
const
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
62
|
+
const addHeaders = (source, keyPrefix) => {
|
|
63
|
+
for (const [key, value] of Object.entries(source)) {
|
|
64
|
+
if (value) {
|
|
65
|
+
headers[keyPrefix + key] = Array.isArray(value)
|
|
66
|
+
? value.join(",")
|
|
67
|
+
: value;
|
|
68
|
+
}
|
|
58
69
|
}
|
|
59
|
-
}
|
|
70
|
+
};
|
|
71
|
+
addHeaders(middlewareHeaders, isResult ? "" : MIDDLEWARE_HEADER_PREFIX);
|
|
72
|
+
addHeaders(nextConfigHeaders, isResult ? "" : NEXT_CONFIG_HEADER_PREFIX);
|
|
60
73
|
}
|
|
61
74
|
export default async function routingHandler(event, { assetResolver }) {
|
|
62
75
|
try {
|
|
@@ -77,8 +90,7 @@ export default async function routingHandler(event, { assetResolver }) {
|
|
|
77
90
|
delete event.headers[key];
|
|
78
91
|
}
|
|
79
92
|
}
|
|
80
|
-
|
|
81
|
-
let headers = getNextConfigHeaders(event, ConfigHeaders);
|
|
93
|
+
let nextConfigHeaders = getNextConfigHeaders(event, ConfigHeaders);
|
|
82
94
|
let eventOrResult = fixDataPage(event, BuildId);
|
|
83
95
|
if (isInternalResult(eventOrResult)) {
|
|
84
96
|
return eventOrResult;
|
|
@@ -100,17 +112,10 @@ export default async function routingHandler(event, { assetResolver }) {
|
|
|
100
112
|
}
|
|
101
113
|
const middlewareHeadersPrioritized = globalThis.openNextConfig.dangerous
|
|
102
114
|
?.middlewareHeadersOverrideNextConfigHeaders ?? false;
|
|
115
|
+
const middlewareHeaders = middlewareEventOrResult.responseHeaders ?? {};
|
|
103
116
|
if (middlewareHeadersPrioritized) {
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
...middlewareEventOrResult.responseHeaders,
|
|
107
|
-
};
|
|
108
|
-
}
|
|
109
|
-
else {
|
|
110
|
-
headers = {
|
|
111
|
-
...middlewareEventOrResult.responseHeaders,
|
|
112
|
-
...headers,
|
|
113
|
-
};
|
|
117
|
+
// Both are keyed by lowercase name.
|
|
118
|
+
nextConfigHeaders = Object.fromEntries(Object.entries(nextConfigHeaders).filter(([key]) => !Object.hasOwn(middlewareHeaders, key)));
|
|
114
119
|
}
|
|
115
120
|
let isExternalRewrite = middlewareEventOrResult.isExternalRewrite ?? false;
|
|
116
121
|
eventOrResult = middlewareEventOrResult;
|
|
@@ -126,7 +131,7 @@ export default async function routingHandler(event, { assetResolver }) {
|
|
|
126
131
|
if (!isExternalRewrite) {
|
|
127
132
|
const assetResult = await assetResolver?.maybeGetAssetResult?.(eventOrResult);
|
|
128
133
|
if (assetResult) {
|
|
129
|
-
|
|
134
|
+
applyResponseHeaders(assetResult, nextConfigHeaders, middlewareHeaders);
|
|
130
135
|
return assetResult;
|
|
131
136
|
}
|
|
132
137
|
}
|
|
@@ -159,7 +164,7 @@ export default async function routingHandler(event, { assetResolver }) {
|
|
|
159
164
|
const isRouteFoundBeforeAllRewrites = isStaticRoute || isDynamicRoute || isExternalRewrite;
|
|
160
165
|
// Resolve against the final internal pathname after every rewrite phase.
|
|
161
166
|
// These ordered matches are also the authoritative cache owner candidates.
|
|
162
|
-
|
|
167
|
+
let resolvedRoutes = [
|
|
163
168
|
...staticRouteMatcher(eventOrResult.rawPath),
|
|
164
169
|
...dynamicRouteMatcher(eventOrResult.rawPath),
|
|
165
170
|
];
|
|
@@ -169,7 +174,12 @@ export default async function routingHandler(event, { assetResolver }) {
|
|
|
169
174
|
isNextImageRoute ||
|
|
170
175
|
// We need to check again once all rewrites have been applied
|
|
171
176
|
staticRouteMatcher(eventOrResult.rawPath).length > 0 ||
|
|
172
|
-
dynamicRouteMatcher(eventOrResult.rawPath).length > 0)
|
|
177
|
+
dynamicRouteMatcher(eventOrResult.rawPath).length > 0) ||
|
|
178
|
+
// Next.js versions before 16.4.5 treat a param whose value is its placeholder (e.g. `[slug]`) as unresolved.
|
|
179
|
+
// Next.js receives both the rawPath and the URL pathname, which is normalized (e.g. dot segments).
|
|
180
|
+
(!isExternalRewrite &&
|
|
181
|
+
[eventOrResult.rawPath, new URL(eventOrResult.url).pathname].some(hasRouteParamPlaceholder))) {
|
|
182
|
+
resolvedRoutes = [];
|
|
173
183
|
eventOrResult = {
|
|
174
184
|
...eventOrResult,
|
|
175
185
|
rawPath: "/404",
|
|
@@ -186,12 +196,12 @@ export default async function routingHandler(event, { assetResolver }) {
|
|
|
186
196
|
debug("Cache interception enabled");
|
|
187
197
|
eventOrResult = await cacheInterceptor(eventOrResult, resolvedRoutes);
|
|
188
198
|
if (isInternalResult(eventOrResult)) {
|
|
189
|
-
|
|
199
|
+
applyResponseHeaders(eventOrResult, nextConfigHeaders, middlewareHeaders);
|
|
190
200
|
return eventOrResult;
|
|
191
201
|
}
|
|
192
202
|
}
|
|
193
|
-
// We apply the headers
|
|
194
|
-
|
|
203
|
+
// We apply the response headers last
|
|
204
|
+
applyResponseHeaders(eventOrResult, nextConfigHeaders, middlewareHeaders);
|
|
195
205
|
debug("resolvedRoutes", resolvedRoutes);
|
|
196
206
|
return {
|
|
197
207
|
internalEvent: eventOrResult,
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { ServerResponse } from "node:http";
|
|
2
|
+
import { type Writable } from "node:stream";
|
|
3
|
+
/**
|
|
4
|
+
* Largest `content-length` that is verified by buffering the body.
|
|
5
|
+
*
|
|
6
|
+
* It matches the largest response compressed by CloudFront, which only compresses responses with a `content-length`.
|
|
7
|
+
* Larger responses are streamed without `content-length`.
|
|
8
|
+
*/
|
|
9
|
+
export declare const MAX_VERIFIED_CONTENT_LENGTH = 10000000;
|
|
10
|
+
/**
|
|
11
|
+
* Writes the head of a Node.js `ServerResponse` and returns the stream to write the body to.
|
|
12
|
+
*
|
|
13
|
+
* The framing headers set by the app may not match the body that is actually written (e.g. a `fetch()` response
|
|
14
|
+
* body is decoded but keeps the upstream `content-length`). The bytes past a stale `content-length` would stay on
|
|
15
|
+
* the keep-alive connection and be read as the next response.
|
|
16
|
+
*
|
|
17
|
+
* - `transfer-encoding` is always removed, Node.js picks the transfer coding.
|
|
18
|
+
* - A `content-length` that is not a valid length is removed.
|
|
19
|
+
* - `content-length` is kept for HEAD requests and 304 responses, Node.js does not write a body for them.
|
|
20
|
+
* - Otherwise `content-length` is only kept when the body has exactly the declared length. The body is buffered
|
|
21
|
+
* until this is known, and the head is written then, so the first byte is only sent once the whole body is
|
|
22
|
+
* received. When the length doesn't match, or is larger than `MAX_VERIFIED_CONTENT_LENGTH`, `content-length` is
|
|
23
|
+
* removed and Node.js frames the body itself (chunked on HTTP/1.1).
|
|
24
|
+
*
|
|
25
|
+
* The status code and the headers are always validated synchronously, even when the head is written later.
|
|
26
|
+
*
|
|
27
|
+
* @param res The Node.js response.
|
|
28
|
+
* @param method The request method.
|
|
29
|
+
* @param statusCode The response status code.
|
|
30
|
+
* @param headers The response headers, keys are matched case-insensitively.
|
|
31
|
+
* @returns The stream to write the body to.
|
|
32
|
+
* @throws When the status code or a header is invalid, or the headers were already sent (same errors as
|
|
33
|
+
* `res.writeHead()`).
|
|
34
|
+
*/
|
|
35
|
+
export declare function writeResponseHead(res: ServerResponse, method: string | undefined, statusCode: number, headers: Record<string, string>): Writable;
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import { Transform } from "node:stream";
|
|
2
|
+
import { error } from "../adapters/logger";
|
|
3
|
+
/**
|
|
4
|
+
* Largest `content-length` that is verified by buffering the body.
|
|
5
|
+
*
|
|
6
|
+
* It matches the largest response compressed by CloudFront, which only compresses responses with a `content-length`.
|
|
7
|
+
* Larger responses are streamed without `content-length`.
|
|
8
|
+
*/
|
|
9
|
+
export const MAX_VERIFIED_CONTENT_LENGTH = 10_000_000;
|
|
10
|
+
/**
|
|
11
|
+
* Writes the head of a Node.js `ServerResponse` and returns the stream to write the body to.
|
|
12
|
+
*
|
|
13
|
+
* The framing headers set by the app may not match the body that is actually written (e.g. a `fetch()` response
|
|
14
|
+
* body is decoded but keeps the upstream `content-length`). The bytes past a stale `content-length` would stay on
|
|
15
|
+
* the keep-alive connection and be read as the next response.
|
|
16
|
+
*
|
|
17
|
+
* - `transfer-encoding` is always removed, Node.js picks the transfer coding.
|
|
18
|
+
* - A `content-length` that is not a valid length is removed.
|
|
19
|
+
* - `content-length` is kept for HEAD requests and 304 responses, Node.js does not write a body for them.
|
|
20
|
+
* - Otherwise `content-length` is only kept when the body has exactly the declared length. The body is buffered
|
|
21
|
+
* until this is known, and the head is written then, so the first byte is only sent once the whole body is
|
|
22
|
+
* received. When the length doesn't match, or is larger than `MAX_VERIFIED_CONTENT_LENGTH`, `content-length` is
|
|
23
|
+
* removed and Node.js frames the body itself (chunked on HTTP/1.1).
|
|
24
|
+
*
|
|
25
|
+
* The status code and the headers are always validated synchronously, even when the head is written later.
|
|
26
|
+
*
|
|
27
|
+
* @param res The Node.js response.
|
|
28
|
+
* @param method The request method.
|
|
29
|
+
* @param statusCode The response status code.
|
|
30
|
+
* @param headers The response headers, keys are matched case-insensitively.
|
|
31
|
+
* @returns The stream to write the body to.
|
|
32
|
+
* @throws When the status code or a header is invalid, or the headers were already sent (same errors as
|
|
33
|
+
* `res.writeHead()`).
|
|
34
|
+
*/
|
|
35
|
+
export function writeResponseHead(res, method, statusCode, headers) {
|
|
36
|
+
validateHead(res, statusCode);
|
|
37
|
+
let contentLength;
|
|
38
|
+
for (const [key, value] of Object.entries(headers)) {
|
|
39
|
+
const lowerKey = key.toLowerCase();
|
|
40
|
+
if (lowerKey === "content-length") {
|
|
41
|
+
contentLength = value;
|
|
42
|
+
}
|
|
43
|
+
else if (lowerKey !== "transfer-encoding") {
|
|
44
|
+
// `setHeader()` validates the header like `writeHead()` does
|
|
45
|
+
res.setHeader(key, value);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
const expectedLength = parseContentLength(contentLength);
|
|
49
|
+
if (!Number.isNaN(expectedLength) &&
|
|
50
|
+
(method?.toUpperCase() === "HEAD" || statusCode === 304)) {
|
|
51
|
+
res.setHeader("content-length", String(expectedLength));
|
|
52
|
+
res.writeHead(statusCode);
|
|
53
|
+
res.flushHeaders();
|
|
54
|
+
return res;
|
|
55
|
+
}
|
|
56
|
+
if (statusCode === 204 || !(expectedLength <= MAX_VERIFIED_CONTENT_LENGTH)) {
|
|
57
|
+
res.writeHead(statusCode);
|
|
58
|
+
res.flushHeaders();
|
|
59
|
+
return res;
|
|
60
|
+
}
|
|
61
|
+
const body = new ContentLengthVerifier(res, statusCode, expectedLength);
|
|
62
|
+
body.pipe(res);
|
|
63
|
+
return body;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Checks that the head can be written, the way `res.writeHead()` does.
|
|
67
|
+
*
|
|
68
|
+
* The headers are validated by `res.setHeader()`.
|
|
69
|
+
*
|
|
70
|
+
* @param res The Node.js response.
|
|
71
|
+
* @param statusCode The response status code.
|
|
72
|
+
* @throws {Error} When the headers were already sent (code `ERR_HTTP_HEADERS_SENT`).
|
|
73
|
+
* @throws {RangeError} When the status code is not in 100-999 (code `ERR_HTTP_INVALID_STATUS_CODE`).
|
|
74
|
+
*/
|
|
75
|
+
function validateHead(res, statusCode) {
|
|
76
|
+
if (res.headersSent) {
|
|
77
|
+
throw Object.assign(new Error("Cannot write headers after they are sent to the client"), { code: "ERR_HTTP_HEADERS_SENT" });
|
|
78
|
+
}
|
|
79
|
+
const code = statusCode | 0;
|
|
80
|
+
if (code < 100 || code > 999) {
|
|
81
|
+
throw Object.assign(new RangeError(`Invalid status code: ${statusCode}`), {
|
|
82
|
+
code: "ERR_HTTP_INVALID_STATUS_CODE",
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Parses a `content-length` header value.
|
|
88
|
+
*
|
|
89
|
+
* @param contentLength The header value.
|
|
90
|
+
* @returns The length, or `NaN` when the value is missing or not a valid length.
|
|
91
|
+
*/
|
|
92
|
+
function parseContentLength(contentLength) {
|
|
93
|
+
const value = contentLength?.trim() ?? "";
|
|
94
|
+
const length = /^\d+$/.test(value) ? Number(value) : Number.NaN;
|
|
95
|
+
return Number.isSafeInteger(length) ? length : Number.NaN;
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Buffers the body until it is known whether it has the declared length, then writes the head and the body.
|
|
99
|
+
*
|
|
100
|
+
* The headers, other than `content-length`, are already set on the response.
|
|
101
|
+
*/
|
|
102
|
+
class ContentLengthVerifier extends Transform {
|
|
103
|
+
res;
|
|
104
|
+
statusCode;
|
|
105
|
+
expectedLength;
|
|
106
|
+
chunks = [];
|
|
107
|
+
length = 0;
|
|
108
|
+
headWritten = false;
|
|
109
|
+
/**
|
|
110
|
+
* @param res The Node.js response.
|
|
111
|
+
* @param statusCode The response status code, already validated.
|
|
112
|
+
* @param expectedLength The declared `content-length`.
|
|
113
|
+
*/
|
|
114
|
+
constructor(res, statusCode, expectedLength) {
|
|
115
|
+
super();
|
|
116
|
+
this.res = res;
|
|
117
|
+
this.statusCode = statusCode;
|
|
118
|
+
this.expectedLength = expectedLength;
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Buffers the chunk until the head is written, then passes it through.
|
|
122
|
+
*
|
|
123
|
+
* The head is written without `content-length` as soon as the body is longer than declared.
|
|
124
|
+
*
|
|
125
|
+
* @param chunk The body chunk.
|
|
126
|
+
* @param _encoding Unused, chunks are buffers.
|
|
127
|
+
* @param callback Called once the chunk is processed.
|
|
128
|
+
*/
|
|
129
|
+
_transform(chunk, _encoding, callback) {
|
|
130
|
+
if (this.headWritten) {
|
|
131
|
+
callback(null, chunk);
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
this.chunks.push(chunk);
|
|
135
|
+
this.length += chunk.length;
|
|
136
|
+
if (this.length > this.expectedLength) {
|
|
137
|
+
this.writeHead(false);
|
|
138
|
+
}
|
|
139
|
+
callback();
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Writes the head, when not already written, once the whole body is received.
|
|
143
|
+
*
|
|
144
|
+
* @param callback Called once the head and the buffered chunks are written.
|
|
145
|
+
*/
|
|
146
|
+
_flush(callback) {
|
|
147
|
+
if (!this.headWritten) {
|
|
148
|
+
this.writeHead(this.length === this.expectedLength);
|
|
149
|
+
}
|
|
150
|
+
callback();
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Writes the head and the buffered chunks.
|
|
154
|
+
*
|
|
155
|
+
* This runs from the stream callbacks, where a thrown error would be uncaught and crash the process. The
|
|
156
|
+
* head is validated synchronously in `writeResponseHead()`, if writing it still fails, the response is destroyed.
|
|
157
|
+
*
|
|
158
|
+
* @param keepContentLength Whether the body has the declared length.
|
|
159
|
+
*/
|
|
160
|
+
writeHead(keepContentLength) {
|
|
161
|
+
this.headWritten = true;
|
|
162
|
+
const chunks = this.chunks;
|
|
163
|
+
this.chunks = [];
|
|
164
|
+
try {
|
|
165
|
+
if (keepContentLength) {
|
|
166
|
+
this.res.setHeader("content-length", String(this.expectedLength));
|
|
167
|
+
}
|
|
168
|
+
this.res.writeHead(this.statusCode);
|
|
169
|
+
}
|
|
170
|
+
catch (e) {
|
|
171
|
+
error("Failed to write the response head", e);
|
|
172
|
+
this.res.destroy();
|
|
173
|
+
// Without an error, no `error` event is emitted
|
|
174
|
+
this.destroy();
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
for (const chunk of chunks) {
|
|
178
|
+
this.push(chunk);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
package/dist/http/util.d.ts
CHANGED
|
@@ -8,6 +8,17 @@ export declare const convertHeader: (header: http.OutgoingHttpHeader) => string;
|
|
|
8
8
|
* @returns A list of Set-Cookie header
|
|
9
9
|
*/
|
|
10
10
|
export declare function parseSetCookieHeader(cookies: string | string[] | null | undefined): string[];
|
|
11
|
+
/**
|
|
12
|
+
* Converts the headers of a response returned by an edge function.
|
|
13
|
+
*
|
|
14
|
+
* `Set-Cookie` headers are returned as a list.
|
|
15
|
+
* The internal `x-middleware-set-cookie` header is dropped: `NextResponse` sets it when
|
|
16
|
+
* `response.cookies` is modified (i.e. in a route handler) and it contains the `HttpOnly` cookies.
|
|
17
|
+
*
|
|
18
|
+
* @param headers The headers of the response
|
|
19
|
+
* @returns The response headers to send to the client
|
|
20
|
+
*/
|
|
21
|
+
export declare function convertEdgeResponseHeaders(headers: Pick<Headers, "forEach" | "getSetCookie">): Record<string, string | string[]>;
|
|
11
22
|
/**
|
|
12
23
|
*
|
|
13
24
|
* Get the query object from an iterable of [key, value] pairs
|
package/dist/http/util.js
CHANGED
|
@@ -58,6 +58,32 @@ export function parseSetCookieHeader(cookies) {
|
|
|
58
58
|
}
|
|
59
59
|
return cookies;
|
|
60
60
|
}
|
|
61
|
+
/**
|
|
62
|
+
* Converts the headers of a response returned by an edge function.
|
|
63
|
+
*
|
|
64
|
+
* `Set-Cookie` headers are returned as a list.
|
|
65
|
+
* The internal `x-middleware-set-cookie` header is dropped: `NextResponse` sets it when
|
|
66
|
+
* `response.cookies` is modified (i.e. in a route handler) and it contains the `HttpOnly` cookies.
|
|
67
|
+
*
|
|
68
|
+
* @param headers The headers of the response
|
|
69
|
+
* @returns The response headers to send to the client
|
|
70
|
+
*/
|
|
71
|
+
export function convertEdgeResponseHeaders(headers) {
|
|
72
|
+
const responseHeaders = {};
|
|
73
|
+
headers.forEach((value, key) => {
|
|
74
|
+
const lowerKey = key.toLowerCase();
|
|
75
|
+
// Headers.forEach folds same-name headers; use getSetCookie() below instead.
|
|
76
|
+
if (lowerKey === "set-cookie" || lowerKey === "x-middleware-set-cookie") {
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
responseHeaders[key] = value;
|
|
80
|
+
});
|
|
81
|
+
const setCookies = headers.getSetCookie();
|
|
82
|
+
if (setCookies.length > 0) {
|
|
83
|
+
responseHeaders["set-cookie"] = setCookies;
|
|
84
|
+
}
|
|
85
|
+
return responseHeaders;
|
|
86
|
+
}
|
|
61
87
|
/**
|
|
62
88
|
*
|
|
63
89
|
* Get the query object from an iterable of [key, value] pairs
|
package/dist/minimize-js.d.ts
CHANGED
|
@@ -1 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minifies the JS (and optionally JSON) files of a directory in place.
|
|
3
|
+
*
|
|
4
|
+
* Calls can run concurrently. Failed files are logged and do not throw.
|
|
5
|
+
*
|
|
6
|
+
* @param dir Directory to walk recursively.
|
|
7
|
+
* @param opts Minification options.
|
|
8
|
+
* @return A promise that resolves once all the files have been processed.
|
|
9
|
+
*/
|
|
1
10
|
export declare function minifyAll(dir: any, opts: any): Promise<void>;
|
package/dist/minimize-js.js
CHANGED
|
@@ -4,11 +4,8 @@
|
|
|
4
4
|
// @ts-nocheck
|
|
5
5
|
import fs from "node:fs/promises";
|
|
6
6
|
import path from "node:path";
|
|
7
|
-
import minify from "@node-minify/core";
|
|
8
|
-
import terser from "@node-minify/terser";
|
|
9
|
-
const failed_files = [];
|
|
10
|
-
let total_files = 0;
|
|
11
|
-
const options = {};
|
|
7
|
+
import { minify } from "@node-minify/core";
|
|
8
|
+
import { terser } from "@node-minify/terser";
|
|
12
9
|
const promiseSeries = async (tasks, initial) => {
|
|
13
10
|
if (!Array.isArray(tasks)) {
|
|
14
11
|
return Promise.reject(new TypeError("promise.series only accepts an array of functions"));
|
|
@@ -17,29 +14,28 @@ const promiseSeries = async (tasks, initial) => {
|
|
|
17
14
|
return current.then(next);
|
|
18
15
|
}, Promise.resolve(initial));
|
|
19
16
|
};
|
|
20
|
-
const minifyJS = async (file) => {
|
|
21
|
-
total_files++;
|
|
17
|
+
const minifyJS = async (file, ctx) => {
|
|
22
18
|
try {
|
|
23
19
|
await minify({
|
|
24
20
|
compressor: terser,
|
|
25
21
|
input: file,
|
|
26
22
|
output: file,
|
|
23
|
+
// node-minify 10 throws when the output is empty (i.e. empty or comment-only files)
|
|
24
|
+
allowEmptyOutput: true,
|
|
27
25
|
options: {
|
|
28
|
-
module: options.module,
|
|
29
|
-
mangle: options.mangle,
|
|
26
|
+
module: ctx.options.module,
|
|
27
|
+
mangle: ctx.options.mangle,
|
|
30
28
|
compress: { reduce_vars: false },
|
|
31
29
|
},
|
|
32
30
|
});
|
|
33
31
|
}
|
|
34
32
|
catch (e) {
|
|
35
|
-
failed_files.push(file);
|
|
33
|
+
ctx.failed_files.push(file);
|
|
36
34
|
}
|
|
37
|
-
//process.stdout.write(".");
|
|
38
35
|
};
|
|
39
|
-
const minifyJSON = async (file) => {
|
|
36
|
+
const minifyJSON = async (file, options) => {
|
|
40
37
|
try {
|
|
41
38
|
if (options.compress_json || options.packagejson) {
|
|
42
|
-
total_files++;
|
|
43
39
|
const is_package_json = file.indexOf("package.json") > -1;
|
|
44
40
|
const data = await fs.readFile(file, "utf8");
|
|
45
41
|
const json = JSON.parse(data);
|
|
@@ -63,9 +59,8 @@ const minifyJSON = async (file) => {
|
|
|
63
59
|
}
|
|
64
60
|
}
|
|
65
61
|
catch (e) { }
|
|
66
|
-
//process.stdout.write(".");
|
|
67
62
|
};
|
|
68
|
-
const walk = async (currentDirPath) => {
|
|
63
|
+
const walk = async (currentDirPath, ctx) => {
|
|
69
64
|
const js_files = [];
|
|
70
65
|
const json_files = [];
|
|
71
66
|
const dirs = [];
|
|
@@ -77,26 +72,32 @@ const walk = async (currentDirPath) => {
|
|
|
77
72
|
if (stat.isFile()) {
|
|
78
73
|
if (filePath.substr(-5) === ".json")
|
|
79
74
|
json_files.push(filePath);
|
|
80
|
-
else if (filePath.substr(-3) === ".js" || options.all_js)
|
|
75
|
+
else if (filePath.substr(-3) === ".js" || ctx.options.all_js)
|
|
81
76
|
js_files.push(filePath);
|
|
82
77
|
}
|
|
83
78
|
else if (stat.isDirectory() && !is_bin.test(filePath)) {
|
|
84
79
|
dirs.push(filePath);
|
|
85
80
|
}
|
|
86
81
|
}
|
|
87
|
-
const js_promise = Promise.all(js_files.map((f) => minifyJS(f)));
|
|
88
|
-
const json_promise = Promise.all(json_files.map((f) => minifyJSON(f)));
|
|
82
|
+
const js_promise = Promise.all(js_files.map((f) => minifyJS(f, ctx)));
|
|
83
|
+
const json_promise = Promise.all(json_files.map((f) => minifyJSON(f, ctx.options)));
|
|
89
84
|
await Promise.all([js_promise, json_promise]);
|
|
90
|
-
await promiseSeries(dirs.map((dir) => () => walk(dir)));
|
|
85
|
+
await promiseSeries(dirs.map((dir) => () => walk(dir, ctx)));
|
|
91
86
|
};
|
|
87
|
+
/**
|
|
88
|
+
* Minifies the JS (and optionally JSON) files of a directory in place.
|
|
89
|
+
*
|
|
90
|
+
* Calls can run concurrently. Failed files are logged and do not throw.
|
|
91
|
+
*
|
|
92
|
+
* @param dir Directory to walk recursively.
|
|
93
|
+
* @param opts Minification options.
|
|
94
|
+
* @return A promise that resolves once all the files have been processed.
|
|
95
|
+
*/
|
|
92
96
|
export async function minifyAll(dir, opts) {
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
//process.stdout.write(".\n");
|
|
97
|
-
//console.log("Total found files: " + total_files);
|
|
98
|
-
if (failed_files.length) {
|
|
97
|
+
const ctx = { options: { ...opts }, failed_files: [] };
|
|
98
|
+
await walk(dir, ctx);
|
|
99
|
+
if (ctx.failed_files.length) {
|
|
99
100
|
console.log("\n\nFailed to minify files:");
|
|
100
|
-
failed_files.forEach((f) => console.log(`\t${f}`));
|
|
101
|
+
ctx.failed_files.forEach((f) => console.log(`\t${f}`));
|
|
101
102
|
}
|
|
102
103
|
}
|
|
@@ -4,7 +4,11 @@ const fetchProxy = {
|
|
|
4
4
|
// @ts-ignore
|
|
5
5
|
proxy: async (internalEvent) => {
|
|
6
6
|
const { url, headers: eventHeaders, method, body } = internalEvent;
|
|
7
|
-
const headers = Object.fromEntries(Object.entries(eventHeaders).filter(([key]) =>
|
|
7
|
+
const headers = Object.fromEntries(Object.entries(eventHeaders).filter(([key]) => !["cf-connecting-ip", "x-forwarded-host"].includes(key.toLowerCase())));
|
|
8
|
+
// Like Next.js, do not relay the `x-forwarded-host` sent by the client
|
|
9
|
+
if (eventHeaders.host) {
|
|
10
|
+
headers["x-forwarded-host"] = eventHeaders.host;
|
|
11
|
+
}
|
|
8
12
|
const response = await fetch(url, {
|
|
9
13
|
method,
|
|
10
14
|
headers,
|
|
@@ -6,6 +6,7 @@ function filterHeadersForProxy(headers) {
|
|
|
6
6
|
const filteredHeaders = {};
|
|
7
7
|
const disallowedHeaders = [
|
|
8
8
|
"host",
|
|
9
|
+
"x-forwarded-host",
|
|
9
10
|
"connection",
|
|
10
11
|
"via",
|
|
11
12
|
"x-cache",
|
|
@@ -30,6 +31,10 @@ const nodeProxy = {
|
|
|
30
31
|
debug("proxyRequest", url);
|
|
31
32
|
return new Promise((resolve, reject) => {
|
|
32
33
|
const filteredHeaders = filterHeadersForProxy(headers);
|
|
34
|
+
// Like Next.js, do not relay the `x-forwarded-host` sent by the client
|
|
35
|
+
if (headers.host) {
|
|
36
|
+
filteredHeaders["x-forwarded-host"] = headers.host;
|
|
37
|
+
}
|
|
33
38
|
debug("filteredHeaders", filteredHeaders);
|
|
34
39
|
const req = request(url, {
|
|
35
40
|
headers: filteredHeaders,
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
2
|
import express from "express";
|
|
3
3
|
import { NextConfig } from "../../adapters/config/index.js";
|
|
4
|
+
import { writeResponseHead } from "../../http/framing.js";
|
|
4
5
|
import { getMonorepoRelativePath } from "../../utils/normalize-path.js";
|
|
5
6
|
const wrapper = async (handler, converter) => {
|
|
6
7
|
const app = express();
|
|
@@ -16,8 +17,7 @@ const wrapper = async (handler, converter) => {
|
|
|
16
17
|
const internalEvent = await converter.convertFrom(req);
|
|
17
18
|
const streamCreator = {
|
|
18
19
|
writeHeaders: (prelude) => {
|
|
19
|
-
res.
|
|
20
|
-
return res;
|
|
20
|
+
return writeResponseHead(res, req.method, prelude.statusCode, prelude.headers);
|
|
21
21
|
},
|
|
22
22
|
};
|
|
23
23
|
await imageHandler(internalEvent, { streamCreator });
|
|
@@ -34,9 +34,7 @@ const wrapper = async (handler, converter) => {
|
|
|
34
34
|
const streamCreator = {
|
|
35
35
|
writeHeaders: (prelude) => {
|
|
36
36
|
res.setHeader("Set-Cookie", prelude.cookies);
|
|
37
|
-
res.
|
|
38
|
-
res.flushHeaders();
|
|
39
|
-
return res;
|
|
37
|
+
return writeResponseHead(res, req.method, prelude.statusCode, prelude.headers);
|
|
40
38
|
},
|
|
41
39
|
onFinish: () => { },
|
|
42
40
|
abortSignal: abortController.signal,
|