vinext 1.0.1 → 1.1.0
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/README.md +72 -1
- package/dist/build/app-page-static-eligibility.d.ts +15 -0
- package/dist/build/app-page-static-eligibility.js +82 -0
- package/dist/build/prerender-paths.js +3 -78
- package/dist/build/prerender.js +33 -8
- package/dist/build/server-manifest.d.ts +7 -0
- package/dist/build/server-manifest.js +11 -1
- package/dist/cache/cache-adapters-virtual.d.ts +7 -1
- package/dist/cache/cache-adapters-virtual.js +3 -3
- package/dist/client/navigation-runtime.d.ts +11 -0
- package/dist/client/navigation-runtime.js +1 -1
- package/dist/config/config-matchers.js +3 -1
- package/dist/config/next-config.d.ts +7 -0
- package/dist/config/next-config.js +18 -3
- package/dist/entries/app-rsc-entry.d.ts +2 -0
- package/dist/entries/app-rsc-entry.js +64 -43
- package/dist/index.d.ts +4 -1
- package/dist/index.js +59 -30
- package/dist/plugins/cache-flight-codec.d.ts +19 -0
- package/dist/plugins/cache-flight-codec.js +68 -0
- package/dist/plugins/commonjs-esm-facade.d.ts +35 -0
- package/dist/plugins/commonjs-esm-facade.js +80 -0
- package/dist/plugins/fonts.d.ts +8 -11
- package/dist/plugins/fonts.js +71 -34
- package/dist/routing/file-matcher.d.ts +1 -0
- package/dist/routing/file-matcher.js +1 -1
- package/dist/server/api-handler.js +17 -7
- package/dist/server/app-browser-entry.js +11 -7
- package/dist/server/app-browser-history-controller.d.ts +2 -0
- package/dist/server/app-browser-history-controller.js +8 -0
- package/dist/server/app-browser-interception-context.js +26 -2
- package/dist/server/app-browser-navigation-controller.d.ts +7 -1
- package/dist/server/app-browser-navigation-controller.js +93 -10
- package/dist/server/app-fallback-renderer.d.ts +2 -0
- package/dist/server/app-fallback-renderer.js +1 -0
- package/dist/server/app-history-state.d.ts +3 -0
- package/dist/server/app-history-state.js +7 -0
- package/dist/server/app-middleware.js +2 -1
- package/dist/server/app-optimistic-routing.js +1 -1
- package/dist/server/app-page-boundary-render.d.ts +5 -0
- package/dist/server/app-page-boundary-render.js +6 -4
- package/dist/server/app-page-cache-finalizer.d.ts +11 -0
- package/dist/server/app-page-cache-finalizer.js +41 -7
- package/dist/server/app-page-cache-render.d.ts +18 -0
- package/dist/server/app-page-cache-render.js +63 -43
- package/dist/server/app-page-cache.d.ts +5 -0
- package/dist/server/app-page-cache.js +27 -7
- package/dist/server/app-page-dispatch.d.ts +9 -1
- package/dist/server/app-page-dispatch.js +47 -30
- package/dist/server/app-page-element-builder.d.ts +7 -0
- package/dist/server/app-page-element-builder.js +27 -42
- package/dist/server/app-page-execution.d.ts +15 -12
- package/dist/server/app-page-execution.js +30 -40
- package/dist/server/app-page-head.d.ts +5 -0
- package/dist/server/app-page-head.js +9 -3
- package/dist/server/app-page-probe.d.ts +0 -109
- package/dist/server/app-page-probe.js +3 -136
- package/dist/server/app-page-render-identity.d.ts +0 -1
- package/dist/server/app-page-render-identity.js +1 -1
- package/dist/server/app-page-render-observation.js +2 -2
- package/dist/server/app-page-render.d.ts +13 -5
- package/dist/server/app-page-render.js +219 -119
- package/dist/server/app-page-request.d.ts +0 -1
- package/dist/server/app-page-request.js +1 -2
- package/dist/server/app-page-response.d.ts +4 -3
- package/dist/server/app-page-response.js +4 -3
- package/dist/server/app-page-route-wiring.d.ts +17 -5
- package/dist/server/app-page-route-wiring.js +55 -33
- package/dist/server/app-page-rsc-render-status.d.ts +48 -0
- package/dist/server/app-page-rsc-render-status.js +54 -0
- package/dist/server/app-page-stream.d.ts +15 -8
- package/dist/server/app-page-stream.js +6 -0
- package/dist/server/app-request-stage-dispatch.js +1 -1
- package/dist/server/app-request-stage-independent-entry.js +4 -4
- package/dist/server/app-response-header-provenance.d.ts +2 -0
- package/dist/server/app-response-header-provenance.js +10 -4
- package/dist/server/app-route-handler-dispatch.js +1 -1
- package/dist/server/app-router-entry.js +3 -3
- package/dist/server/app-rsc-handler.d.ts +4 -0
- package/dist/server/app-rsc-handler.js +212 -96
- package/dist/server/app-rsc-request-normalization.d.ts +3 -1
- package/dist/server/app-rsc-request-normalization.js +4 -2
- package/dist/server/app-rsc-response-stage.d.ts +1 -1
- package/dist/server/app-rsc-response-stage.js +6 -4
- package/dist/server/app-rsc-route-matching.d.ts +9 -1
- package/dist/server/app-rsc-route-matching.js +36 -19
- package/dist/server/app-server-action-execution.js +1 -1
- package/dist/server/app-ssr-entry.d.ts +3 -2
- package/dist/server/app-ssr-entry.js +6 -4
- package/dist/server/app-ssr-stream.d.ts +6 -1
- package/dist/server/app-ssr-stream.js +34 -15
- package/dist/server/app-worker-stages.d.ts +2 -2
- package/dist/server/app-worker-stages.js +3 -3
- package/dist/server/cacheability-request.js +9 -5
- package/dist/server/config-headers.js +2 -0
- package/dist/server/dev-server.js +12 -2
- package/dist/server/headers.d.ts +14 -0
- package/dist/server/headers.js +15 -1
- package/dist/server/image-optimization.d.ts +16 -0
- package/dist/server/image-optimization.js +26 -6
- package/dist/server/isr-cache.js +2 -2
- package/dist/server/middleware-matcher-pattern.js +8 -4
- package/dist/server/middleware-response-headers.js +2 -0
- package/dist/server/multi-stage.d.ts +8 -0
- package/dist/server/navigation-planner.js +1 -1
- package/dist/server/node-response-signal.d.ts +16 -0
- package/dist/server/node-response-signal.js +26 -0
- package/dist/server/open-redirect.d.ts +47 -0
- package/dist/server/open-redirect.js +74 -1
- package/dist/server/pages-page-handler.js +21 -4
- package/dist/server/pages-request-stage-entry.js +8 -4
- package/dist/server/prod-server.d.ts +8 -7
- package/dist/server/prod-server.js +153 -69
- package/dist/server/request-pipeline.d.ts +20 -8
- package/dist/server/request-pipeline.js +21 -7
- package/dist/server/response-compression.d.ts +43 -0
- package/dist/server/response-compression.js +111 -0
- package/dist/server/seed-cache.js +10 -7
- package/dist/server/socket-error-backstop.d.ts +14 -0
- package/dist/server/socket-error-backstop.js +27 -4
- package/dist/shims/cache-adapter-instantiate.d.ts +50 -0
- package/dist/shims/cache-adapter-instantiate.js +118 -0
- package/dist/shims/cache-callable-runtime.d.ts +2 -40
- package/dist/shims/cache-callable-runtime.js +50 -168
- package/dist/shims/cache-flight-arguments.d.ts +22 -0
- package/dist/shims/cache-flight-arguments.js +56 -0
- package/dist/shims/cache-request-state.d.ts +15 -0
- package/dist/shims/cache-request-state.js +31 -8
- package/dist/shims/cache-runtime.d.ts +6 -19
- package/dist/shims/cache-runtime.js +165 -225
- package/dist/shims/cache.d.ts +2 -2
- package/dist/shims/cache.js +5 -4
- package/dist/shims/cacheability-classification.d.ts +7 -0
- package/dist/shims/error-boundary-navigation.d.ts +7 -0
- package/dist/shims/error-boundary-navigation.js +19 -1
- package/dist/shims/error-boundary.d.ts +10 -10
- package/dist/shims/error-boundary.js +26 -7
- package/dist/shims/fetch-cache.d.ts +6 -0
- package/dist/shims/fetch-cache.js +19 -1
- package/dist/shims/form.js +1 -1
- package/dist/shims/headers.d.ts +13 -0
- package/dist/shims/headers.js +47 -4
- package/dist/shims/internal/app-page-props-cache-key.d.ts +5 -2
- package/dist/shims/internal/app-page-props-cache-key.js +24 -15
- package/dist/shims/internal/headers-state.d.ts +0 -1
- package/dist/shims/metadata.js +3 -3
- package/dist/shims/navigation-errors.d.ts +6 -0
- package/dist/shims/navigation-errors.js +11 -1
- package/dist/shims/navigation.d.ts +7 -0
- package/dist/shims/navigation.js +14 -4
- package/dist/shims/request-context.d.ts +9 -0
- package/dist/shims/thenable-params.d.ts +4 -0
- package/dist/shims/thenable-params.js +25 -3
- package/dist/shims/unified-request-context.js +1 -0
- package/dist/utils/mdx-scan.d.ts +5 -0
- package/dist/utils/mdx-scan.js +8 -1
- package/dist/utils/regex-safety.d.ts +10 -0
- package/dist/utils/regex-safety.js +70 -18
- package/package.json +3 -3
|
@@ -5,18 +5,20 @@ import { ASSET_PREFIX_URL_DIR, assetPrefixPathname, isAbsoluteAssetPrefix } from
|
|
|
5
5
|
import { StaticFileCache, contentTypeForPath, etagFromFilenameHash } from "./static-file-cache.js";
|
|
6
6
|
import { normalizePathnameForRouteMatchStrict } from "../routing/utils.js";
|
|
7
7
|
import { hasBasePath, stripBasePath } from "../utils/base-path.js";
|
|
8
|
-
import { VINEXT_PRERENDER_RENDER_ERROR_HEADER } from "./headers.js";
|
|
8
|
+
import { VINEXT_PRERENDER_RENDER_ERROR_HEADER, VINEXT_PRERENDER_SPECIAL_ERROR_HEADER } from "./headers.js";
|
|
9
9
|
import { normalizePath } from "./normalize-path.js";
|
|
10
10
|
import { notFoundResponse } from "./http-error-responses.js";
|
|
11
|
-
import { isOpenRedirectShaped } from "./open-redirect.js";
|
|
11
|
+
import { isOpenRedirectShaped, sendRepeatedSlashRedirect } from "./open-redirect.js";
|
|
12
12
|
import { readStaticFileSignal } from "./static-file-signal.js";
|
|
13
13
|
import { canonicalizeRequestPathname, filterInternalHeaders } from "./request-pipeline.js";
|
|
14
|
+
import { buildCacheStateHeaders } from "./cache-headers.js";
|
|
14
15
|
import { parseHttpDate } from "./http-date.js";
|
|
15
16
|
import { evaluateStaticPreconditions } from "./http-conditional.js";
|
|
16
17
|
import { buildNextDataNotFoundResponse, encodeUrlParserIgnoredCharacters, isNextDataPathname, normalizeNextDataPagePathname, parseNextDataPathname, shouldAddTrailingSlashToPagesDataPath, urlParserCreatesPagesDataPath } from "./pages-data-route.js";
|
|
17
18
|
import { setPagesClientAssets } from "./pages-client-assets.js";
|
|
18
19
|
import { resolveRequestHost, resolveRequestProtocol, trustProxy, trustedHosts } from "./proxy-trust.js";
|
|
19
|
-
import {
|
|
20
|
+
import { signalFromNodeResponse } from "./node-response-signal.js";
|
|
21
|
+
import { DEFAULT_DEVICE_SIZES, DEFAULT_IMAGE_SIZES, IMAGE_RESPONSE_CACHE_STATE, isImageOptimizationPath, isSafeImageContentType, parseImageParams } from "./image-optimization.js";
|
|
20
22
|
import { installSocketErrorBackstop } from "./socket-error-backstop.js";
|
|
21
23
|
import { collectInlineCssManifest } from "../build/inline-css.js";
|
|
22
24
|
import { readTrustedRevalidationHostname } from "./revalidation-host.js";
|
|
@@ -24,11 +26,12 @@ import { traceFrameworkRequest } from "./request-tracing.js";
|
|
|
24
26
|
import { mergeHeaders } from "./worker-utils.js";
|
|
25
27
|
import { runPagesRequest, wrapMiddlewareWithBasePath } from "./pages-request-pipeline.js";
|
|
26
28
|
import { computeClientRuntimeMetadata } from "../utils/client-runtime-metadata.js";
|
|
27
|
-
import { readPrerenderSecret } from "../build/server-manifest.js";
|
|
29
|
+
import { readPrerenderSecret, readServerCompress } from "../build/server-manifest.js";
|
|
28
30
|
import { readTrustedPrerenderRouteParamsFromHeaders, serializePrerenderRouteParamsHeader } from "./prerender-route-params.js";
|
|
29
31
|
import { registerPrerenderCloudflareLoader } from "../build/prerender-cloudflare-loader.js";
|
|
30
32
|
import { seedMemoryCacheFromPrerender } from "./seed-cache.js";
|
|
31
33
|
import { negotiateEncoding, parseAcceptedEncodings, selectContentEncoding } from "./accept-encoding.js";
|
|
34
|
+
import { COMPRESS_THRESHOLD, hasNoTransform, isCompressibleContentType, parseContentLengthHeader, resolveResponseCompression } from "./response-compression.js";
|
|
32
35
|
import { ifRangeAllowsRange, parseByteRange } from "./http-range.js";
|
|
33
36
|
import { createRequire } from "node:module";
|
|
34
37
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
@@ -192,25 +195,6 @@ function readNodeStream(req) {
|
|
|
192
195
|
}
|
|
193
196
|
});
|
|
194
197
|
}
|
|
195
|
-
/** Content types that benefit from compression. */
|
|
196
|
-
const COMPRESSIBLE_TYPES = /* @__PURE__ */ new Set([
|
|
197
|
-
"text/html",
|
|
198
|
-
"text/css",
|
|
199
|
-
"text/plain",
|
|
200
|
-
"text/xml",
|
|
201
|
-
"text/javascript",
|
|
202
|
-
"application/javascript",
|
|
203
|
-
"application/json",
|
|
204
|
-
"application/xml",
|
|
205
|
-
"application/xhtml+xml",
|
|
206
|
-
"application/rss+xml",
|
|
207
|
-
"application/atom+xml",
|
|
208
|
-
"image/svg+xml",
|
|
209
|
-
"application/manifest+json",
|
|
210
|
-
"application/wasm"
|
|
211
|
-
]);
|
|
212
|
-
/** Minimum size threshold for compression (in bytes). Below this, compression overhead isn't worth it. */
|
|
213
|
-
const COMPRESS_THRESHOLD = 1024;
|
|
214
198
|
/**
|
|
215
199
|
* Create a compression stream for the given encoding.
|
|
216
200
|
*/
|
|
@@ -276,12 +260,14 @@ const NO_BODY_RESPONSE_STATUSES = /* @__PURE__ */ new Set([
|
|
|
276
260
|
205,
|
|
277
261
|
304
|
|
278
262
|
]);
|
|
263
|
+
const IMAGE_REPRESENTATION_HEADERS = buildCacheStateHeaders(IMAGE_RESPONSE_CACHE_STATE);
|
|
279
264
|
const OMIT_BODY_HEADERS = /* @__PURE__ */ new Set(["content-length", "content-type"]);
|
|
280
265
|
const OMIT_STATIC_RESPONSE_HEADERS = /* @__PURE__ */ new Set([
|
|
281
266
|
"content-encoding",
|
|
282
267
|
"content-length",
|
|
283
268
|
"content-range",
|
|
284
|
-
"content-type"
|
|
269
|
+
"content-type",
|
|
270
|
+
VINEXT_PRERENDER_SPECIAL_ERROR_HEADER
|
|
285
271
|
]);
|
|
286
272
|
function omitHeadersCaseInsensitive(headersRecord, targets) {
|
|
287
273
|
const filtered = {};
|
|
@@ -291,6 +277,23 @@ function omitHeadersCaseInsensitive(headersRecord, targets) {
|
|
|
291
277
|
}
|
|
292
278
|
return filtered;
|
|
293
279
|
}
|
|
280
|
+
function readHeaderCaseInsensitive(headersRecord, name) {
|
|
281
|
+
const key = Object.keys(headersRecord).find((entry) => entry.toLowerCase() === name);
|
|
282
|
+
if (key === void 0) return void 0;
|
|
283
|
+
const value = headersRecord[key];
|
|
284
|
+
return Array.isArray(value) ? value.join(", ") : value;
|
|
285
|
+
}
|
|
286
|
+
/** Remove one field name from a Vary header, dropping the header if it empties. */
|
|
287
|
+
function omitVaryToken(headers, token) {
|
|
288
|
+
const varyKey = Object.keys(headers).find((key) => key.toLowerCase() === "vary");
|
|
289
|
+
if (varyKey === void 0) return headers;
|
|
290
|
+
const rawVary = headers[varyKey];
|
|
291
|
+
const remaining = (Array.isArray(rawVary) ? rawVary.join(",") : rawVary).split(",").map((entry) => entry.trim()).filter((entry) => entry.length > 0 && entry.toLowerCase() !== token.toLowerCase());
|
|
292
|
+
const result = { ...headers };
|
|
293
|
+
delete result[varyKey];
|
|
294
|
+
if (remaining.length > 0) result[varyKey] = remaining.join(", ");
|
|
295
|
+
return result;
|
|
296
|
+
}
|
|
294
297
|
function mergeVaryHeader(headers, value) {
|
|
295
298
|
const merged = { ...headers };
|
|
296
299
|
const existingKey = Object.keys(merged).find((key) => key.toLowerCase() === "vary");
|
|
@@ -337,6 +340,32 @@ function cancelResponseBody(response) {
|
|
|
337
340
|
if (!body || body.locked) return;
|
|
338
341
|
body.cancel().catch(() => {});
|
|
339
342
|
}
|
|
343
|
+
/**
|
|
344
|
+
* Buffer a response body, cancelling it if the client disconnects first so a
|
|
345
|
+
* stalled stream does not outlive the connection. Resolves null when aborted.
|
|
346
|
+
*/
|
|
347
|
+
async function bufferResponseBodyUntilAborted(response, signal) {
|
|
348
|
+
const body = response.body;
|
|
349
|
+
if (!body) return Buffer.alloc(0);
|
|
350
|
+
const reader = body.getReader();
|
|
351
|
+
const cancel = () => {
|
|
352
|
+
reader.cancel(signal.reason).catch(() => {});
|
|
353
|
+
};
|
|
354
|
+
if (signal.aborted) cancel();
|
|
355
|
+
else signal.addEventListener("abort", cancel, { once: true });
|
|
356
|
+
const chunks = [];
|
|
357
|
+
try {
|
|
358
|
+
while (true) {
|
|
359
|
+
const result = await reader.read();
|
|
360
|
+
if (result.done) break;
|
|
361
|
+
chunks.push(result.value);
|
|
362
|
+
}
|
|
363
|
+
} finally {
|
|
364
|
+
signal.removeEventListener("abort", cancel);
|
|
365
|
+
reader.releaseLock();
|
|
366
|
+
}
|
|
367
|
+
return signal.aborted ? null : Buffer.concat(chunks);
|
|
368
|
+
}
|
|
340
369
|
function isVinextStreamedHtmlResponse(response) {
|
|
341
370
|
return response.__vinextStreamedHtmlResponse === true;
|
|
342
371
|
}
|
|
@@ -370,24 +399,23 @@ function mergeWebResponse(middlewareHeaders, response, statusOverride) {
|
|
|
370
399
|
*/
|
|
371
400
|
function sendCompressed(req, res, body, contentType, statusCode, extraHeaders = {}, compress = true, statusText) {
|
|
372
401
|
const buf = typeof body === "string" ? Buffer.from(body) : body;
|
|
373
|
-
const baseType = contentType.split(";")[0].trim();
|
|
374
|
-
const varyByEncoding = compress && COMPRESSIBLE_TYPES.has(baseType);
|
|
375
|
-
const encoding = compress ? negotiateEncoding(req) : "identity";
|
|
376
402
|
const headersWithoutBodyHeaders = omitHeadersCaseInsensitive(extraHeaders, OMIT_BODY_HEADERS);
|
|
403
|
+
const { varyAcceptEncoding: varyByEncoding, encoding } = resolveResponseCompression(req, compress, {
|
|
404
|
+
contentType,
|
|
405
|
+
cacheControl: readHeaderCaseInsensitive(extraHeaders, "cache-control"),
|
|
406
|
+
contentEncoding: readHeaderCaseInsensitive(extraHeaders, "content-encoding"),
|
|
407
|
+
contentLength: buf.length
|
|
408
|
+
});
|
|
377
409
|
const writeHead = (headers, responseStatus = statusCode, responseStatusText = statusText) => {
|
|
378
410
|
if (responseStatusText) res.writeHead(responseStatus, responseStatusText, headers);
|
|
379
411
|
else res.writeHead(responseStatus, headers);
|
|
380
412
|
};
|
|
381
|
-
if (encoding !== "identity"
|
|
413
|
+
if (encoding !== "identity") {
|
|
382
414
|
writeHead(mergeVaryHeader({
|
|
383
415
|
...headersWithoutBodyHeaders,
|
|
384
416
|
"Content-Type": contentType,
|
|
385
417
|
"Content-Encoding": encoding
|
|
386
418
|
}, "Accept-Encoding"));
|
|
387
|
-
if (req.method === "HEAD") {
|
|
388
|
-
res.end();
|
|
389
|
-
return;
|
|
390
|
-
}
|
|
391
419
|
const compressor = createCompressor(encoding);
|
|
392
420
|
compressor.end(buf);
|
|
393
421
|
pipeline(compressor, res, () => {});
|
|
@@ -415,8 +443,12 @@ function sendCompressed(req, res, body, contentType, statusCode, extraHeaders =
|
|
|
415
443
|
*
|
|
416
444
|
* Without a cache, falls back to async filesystem probing (still non-blocking,
|
|
417
445
|
* unlike the old sync existsSync/statSync approach).
|
|
446
|
+
*
|
|
447
|
+
* `extraHeaders` go on every response for the file. `representationHeaders` go
|
|
448
|
+
* only on responses that carry the file's bytes (200 and 206), never on a 304,
|
|
449
|
+
* 412, 416 or 405.
|
|
418
450
|
*/
|
|
419
|
-
async function tryServeStatic(req, res, clientDir, pathname, compress, cache, extraHeaders, statusCode) {
|
|
451
|
+
async function tryServeStatic(req, res, clientDir, pathname, compress, cache, extraHeaders, statusCode, representationHeaders) {
|
|
420
452
|
if (pathname === "/") return false;
|
|
421
453
|
const responseStatus = statusCode ?? 200;
|
|
422
454
|
const omitBody = isNoBodyResponseStatus(responseStatus);
|
|
@@ -442,23 +474,27 @@ async function tryServeStatic(req, res, clientDir, pathname, compress, cache, ex
|
|
|
442
474
|
sendStaticMethodNotAllowed(res, extraHeaders);
|
|
443
475
|
return true;
|
|
444
476
|
}
|
|
445
|
-
const
|
|
477
|
+
const encodingAllowed = compress && !hasNoTransform((extraHeaders && readHeaderCaseInsensitive(extraHeaders, "cache-control")) ?? entry.original.headers["Cache-Control"]);
|
|
478
|
+
const rawAe = encodingAllowed ? req.headers["accept-encoding"] : void 0;
|
|
446
479
|
const parsed = typeof rawAe === "string" ? parseAcceptedEncodings(rawAe) : void 0;
|
|
447
480
|
const availableVariants = [
|
|
448
481
|
...entry.zst ? ["zstd"] : [],
|
|
449
482
|
...entry.br ? ["br"] : [],
|
|
450
483
|
...entry.gz ? ["gzip"] : []
|
|
451
484
|
];
|
|
452
|
-
const variesByEncoding =
|
|
485
|
+
const variesByEncoding = encodingAllowed && availableVariants.length > 0;
|
|
453
486
|
const selected = parsed ? selectContentEncoding(parsed, availableVariants) : "identity";
|
|
454
487
|
const variant = selected === "zstd" ? entry.zst : selected === "br" ? entry.br : selected === "gzip" ? entry.gz : entry.original;
|
|
488
|
+
const notModifiedBaseHeaders = encodingAllowed ? entry.notModifiedHeaders : omitVaryToken(entry.notModifiedHeaders, "Accept-Encoding");
|
|
489
|
+
const originalHeaders = encodingAllowed ? entry.original.headers : omitVaryToken(entry.original.headers, "Accept-Encoding");
|
|
490
|
+
const variantHeaders = variant === entry.original ? originalHeaders : variant.headers;
|
|
455
491
|
const validators = extraHeaders ? resolveStaticValidators(entry.etag, entry.mtimeMs, extraHeaders) : void 0;
|
|
456
492
|
const effectiveEtag = validators ? validators.etag : entry.etag;
|
|
457
493
|
const effectiveMtimeMs = validators ? validators.mtimeMs : entry.mtimeMs;
|
|
458
494
|
const preconditionResult = responseStatus === 200 ? evaluateRequestPreconditions(req, effectiveEtag, effectiveMtimeMs) : "proceed";
|
|
459
495
|
if (preconditionResult === "precondition-failed") {
|
|
460
496
|
res.writeHead(412, {
|
|
461
|
-
...
|
|
497
|
+
...notModifiedBaseHeaders,
|
|
462
498
|
...extraHeaders,
|
|
463
499
|
"Content-Type": entry.original.headers["Content-Type"],
|
|
464
500
|
"Accept-Ranges": "bytes"
|
|
@@ -468,10 +504,10 @@ async function tryServeStatic(req, res, clientDir, pathname, compress, cache, ex
|
|
|
468
504
|
}
|
|
469
505
|
if (preconditionResult === "not-modified") {
|
|
470
506
|
const notModifiedHeaders = variesByEncoding ? mergeVaryHeader({
|
|
471
|
-
...
|
|
507
|
+
...notModifiedBaseHeaders,
|
|
472
508
|
...extraHeaders
|
|
473
509
|
}, "Accept-Encoding") : {
|
|
474
|
-
...
|
|
510
|
+
...notModifiedBaseHeaders,
|
|
475
511
|
...extraHeaders
|
|
476
512
|
};
|
|
477
513
|
if (selected !== "identity") notModifiedHeaders["Content-Encoding"] = selected;
|
|
@@ -482,7 +518,7 @@ async function tryServeStatic(req, res, clientDir, pathname, compress, cache, ex
|
|
|
482
518
|
const range = resolveRequestedRange(requestRange, ifRange, entry.original.size, effectiveEtag ?? "", effectiveMtimeMs, responseStatus);
|
|
483
519
|
if (range.kind === "unsatisfiable") {
|
|
484
520
|
res.writeHead(416, {
|
|
485
|
-
...
|
|
521
|
+
...notModifiedBaseHeaders,
|
|
486
522
|
...extraHeaders,
|
|
487
523
|
"Content-Type": entry.original.headers["Content-Type"],
|
|
488
524
|
"Accept-Ranges": "bytes",
|
|
@@ -494,8 +530,9 @@ async function tryServeStatic(req, res, clientDir, pathname, compress, cache, ex
|
|
|
494
530
|
if (range.kind === "range") {
|
|
495
531
|
const length = range.end - range.start + 1;
|
|
496
532
|
const rangeHeaders = {
|
|
497
|
-
...
|
|
533
|
+
...originalHeaders,
|
|
498
534
|
...extraHeaders,
|
|
535
|
+
...representationHeaders,
|
|
499
536
|
"Accept-Ranges": "bytes",
|
|
500
537
|
"Content-Length": String(length),
|
|
501
538
|
"Content-Range": `bytes ${range.start}-${range.end}/${entry.original.size}`
|
|
@@ -510,8 +547,9 @@ async function tryServeStatic(req, res, clientDir, pathname, compress, cache, ex
|
|
|
510
547
|
return true;
|
|
511
548
|
}
|
|
512
549
|
const responseHeaders = {
|
|
513
|
-
...
|
|
550
|
+
...variantHeaders,
|
|
514
551
|
...extraHeaders,
|
|
552
|
+
...representationHeaders,
|
|
515
553
|
"Accept-Ranges": "bytes"
|
|
516
554
|
};
|
|
517
555
|
res.writeHead(responseStatus, variesByEncoding ? mergeVaryHeader(responseHeaders, "Accept-Encoding") : responseHeaders);
|
|
@@ -549,8 +587,6 @@ async function tryServeStatic(req, res, clientDir, pathname, compress, cache, ex
|
|
|
549
587
|
const isHashed = pathname.includes(`/${ASSET_PREFIX_URL_DIR}/`);
|
|
550
588
|
const cacheControl = isHashed ? "public, max-age=31536000, immutable" : "public, max-age=3600";
|
|
551
589
|
const etag = isHashed && etagFromFilenameHash(resolved.path, ext) || `W/"${resolved.size}-${Math.floor(resolved.mtimeMs / 1e3)}"`;
|
|
552
|
-
const baseType = ct.split(";")[0].trim();
|
|
553
|
-
const isCompressible = compress && COMPRESSIBLE_TYPES.has(baseType);
|
|
554
590
|
const baseHeaders = {
|
|
555
591
|
"Content-Type": ct,
|
|
556
592
|
"Cache-Control": cacheControl,
|
|
@@ -559,7 +595,12 @@ async function tryServeStatic(req, res, clientDir, pathname, compress, cache, ex
|
|
|
559
595
|
"Accept-Ranges": "bytes",
|
|
560
596
|
...extraHeaders
|
|
561
597
|
};
|
|
562
|
-
const
|
|
598
|
+
const { varyAcceptEncoding: isCompressible, encoding } = resolveResponseCompression(req, compress, {
|
|
599
|
+
contentType: ct,
|
|
600
|
+
cacheControl: (extraHeaders && readHeaderCaseInsensitive(extraHeaders, "cache-control")) ?? cacheControl,
|
|
601
|
+
contentEncoding: extraHeaders && readHeaderCaseInsensitive(extraHeaders, "content-encoding"),
|
|
602
|
+
contentLength: resolved.size
|
|
603
|
+
});
|
|
563
604
|
const validators = extraHeaders ? resolveStaticValidators(etag, resolved.mtimeMs, extraHeaders) : void 0;
|
|
564
605
|
const effectiveEtag = validators ? validators.etag : etag;
|
|
565
606
|
const effectiveMtimeMs = validators ? validators.mtimeMs : resolved.mtimeMs;
|
|
@@ -595,6 +636,7 @@ async function tryServeStatic(req, res, clientDir, pathname, compress, cache, ex
|
|
|
595
636
|
const length = range.end - range.start + 1;
|
|
596
637
|
const rangeHeaders = {
|
|
597
638
|
...baseHeaders,
|
|
639
|
+
...representationHeaders,
|
|
598
640
|
"Content-Length": String(length),
|
|
599
641
|
"Content-Range": `bytes ${range.start}-${range.end}/${resolved.size}`
|
|
600
642
|
};
|
|
@@ -609,6 +651,7 @@ async function tryServeStatic(req, res, clientDir, pathname, compress, cache, ex
|
|
|
609
651
|
if (isCompressible && encoding !== "identity") {
|
|
610
652
|
res.writeHead(responseStatus, mergeVaryHeader({
|
|
611
653
|
...baseHeaders,
|
|
654
|
+
...representationHeaders,
|
|
612
655
|
"Content-Encoding": encoding
|
|
613
656
|
}, "Accept-Encoding"));
|
|
614
657
|
if (omitBody || req.method === "HEAD") {
|
|
@@ -626,6 +669,7 @@ async function tryServeStatic(req, res, clientDir, pathname, compress, cache, ex
|
|
|
626
669
|
}
|
|
627
670
|
const identityHeaders = {
|
|
628
671
|
...baseHeaders,
|
|
672
|
+
...representationHeaders,
|
|
629
673
|
"Content-Length": String(resolved.size)
|
|
630
674
|
};
|
|
631
675
|
res.writeHead(responseStatus, isCompressible ? mergeVaryHeader(identityHeaders, "Accept-Encoding") : identityHeaders);
|
|
@@ -734,7 +778,7 @@ async function statIfFile(filePath) {
|
|
|
734
778
|
* When `urlOverride` is provided, it is used as the path + query string
|
|
735
779
|
* instead of `req.url`.
|
|
736
780
|
*/
|
|
737
|
-
function nodeToWebRequest(req, urlOverride, prerenderSecret, i18nConfig, authorizeOnDemandRevalidate) {
|
|
781
|
+
function nodeToWebRequest(req, urlOverride, prerenderSecret, i18nConfig, authorizeOnDemandRevalidate, signal) {
|
|
738
782
|
const proto = resolveRequestProtocol(req);
|
|
739
783
|
const rawHeaders = nodeHeadersToWebHeaders(req.headers);
|
|
740
784
|
const revalidationHostname = readTrustedRevalidationHostname(rawHeaders, i18nConfig, authorizeOnDemandRevalidate);
|
|
@@ -751,9 +795,10 @@ function nodeToWebRequest(req, urlOverride, prerenderSecret, i18nConfig, authori
|
|
|
751
795
|
const hasBody = method !== "GET" && method !== "HEAD";
|
|
752
796
|
const init = {
|
|
753
797
|
method,
|
|
754
|
-
headers
|
|
798
|
+
headers,
|
|
799
|
+
signal
|
|
755
800
|
};
|
|
756
|
-
if (hasBody) {
|
|
801
|
+
if (hasBody && !signal?.aborted) {
|
|
757
802
|
init.body = readNodeStream(req);
|
|
758
803
|
init.duplex = "half";
|
|
759
804
|
}
|
|
@@ -776,22 +821,23 @@ async function sendWebResponse(webResponse, req, res, compress) {
|
|
|
776
821
|
if (existing !== void 0) nodeHeaders[key] = Array.isArray(existing) ? [...existing, value] : [existing, value];
|
|
777
822
|
else nodeHeaders[key] = value;
|
|
778
823
|
});
|
|
779
|
-
const
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
}
|
|
785
|
-
const
|
|
786
|
-
const varyByEncoding = compress && !alreadyEncoded && COMPRESSIBLE_TYPES.has(baseType);
|
|
787
|
-
const encoding = compress && !alreadyEncoded ? negotiateEncoding(req) : "identity";
|
|
788
|
-
const shouldCompress = encoding !== "identity" && COMPRESSIBLE_TYPES.has(baseType);
|
|
824
|
+
const { varyAcceptEncoding: varyByEncoding, encoding } = resolveResponseCompression(req, compress, {
|
|
825
|
+
contentType: webResponse.headers.get("content-type"),
|
|
826
|
+
cacheControl: webResponse.headers.get("cache-control"),
|
|
827
|
+
contentEncoding: webResponse.headers.get("content-encoding"),
|
|
828
|
+
contentLength: webResponse.body ? parseContentLengthHeader(webResponse.headers.get("content-length")) : 0
|
|
829
|
+
});
|
|
830
|
+
const shouldCompress = encoding !== "identity";
|
|
789
831
|
if (shouldCompress) {
|
|
790
832
|
delete nodeHeaders["content-length"];
|
|
791
|
-
delete nodeHeaders["
|
|
833
|
+
delete nodeHeaders["content-encoding"];
|
|
792
834
|
nodeHeaders["Content-Encoding"] = encoding;
|
|
793
835
|
}
|
|
794
836
|
writeHead(varyByEncoding ? mergeVaryHeader(nodeHeaders, "Accept-Encoding") : nodeHeaders);
|
|
837
|
+
if (!webResponse.body) {
|
|
838
|
+
res.end();
|
|
839
|
+
return;
|
|
840
|
+
}
|
|
795
841
|
if (req.method === "HEAD") {
|
|
796
842
|
cancelResponseBody(webResponse);
|
|
797
843
|
res.end();
|
|
@@ -861,6 +907,7 @@ async function startProdServer(options = {}) {
|
|
|
861
907
|
port,
|
|
862
908
|
host,
|
|
863
909
|
clientDir,
|
|
910
|
+
serverDir,
|
|
864
911
|
serverEntryPath,
|
|
865
912
|
compress,
|
|
866
913
|
purpose,
|
|
@@ -877,6 +924,23 @@ function createNodeExecutionContext(trustedRevalidateOrigin) {
|
|
|
877
924
|
trustedRevalidateOrigin
|
|
878
925
|
};
|
|
879
926
|
}
|
|
927
|
+
/**
|
|
928
|
+
* Sets the prerender special-error marker only from the page's recorded
|
|
929
|
+
* special error, which middleware, route handlers and external rewrites can't
|
|
930
|
+
* reach, and drops one they set.
|
|
931
|
+
*/
|
|
932
|
+
function markPrerenderSpecialError(response, marker) {
|
|
933
|
+
const trusted = marker !== void 0 && marker.status === response.status;
|
|
934
|
+
if (!trusted && !response.headers.has("x-vinext-prerender-special-error")) return response;
|
|
935
|
+
const headers = new Headers(response.headers);
|
|
936
|
+
headers.delete(VINEXT_PRERENDER_SPECIAL_ERROR_HEADER);
|
|
937
|
+
if (trusted) headers.set(VINEXT_PRERENDER_SPECIAL_ERROR_HEADER, JSON.stringify(marker.headers));
|
|
938
|
+
return new Response(response.body, {
|
|
939
|
+
headers,
|
|
940
|
+
status: response.status,
|
|
941
|
+
statusText: response.statusText
|
|
942
|
+
});
|
|
943
|
+
}
|
|
880
944
|
function resolveTrustedNodeRevalidateOrigin(req, configuredHost, configuredPort) {
|
|
881
945
|
const port = req.socket.localPort ?? configuredPort;
|
|
882
946
|
return `http://${normalizeInternalFetchHost(configuredHost)}:${port}`;
|
|
@@ -1008,8 +1072,9 @@ function installPagesClientAssets(options) {
|
|
|
1008
1072
|
* 4. Stream the Web Response back (with optional compression)
|
|
1009
1073
|
*/
|
|
1010
1074
|
async function startAppRouterServer(options) {
|
|
1011
|
-
const { port, host, clientDir, serverDir, rscEntryPath, compress, purpose, silent } = options;
|
|
1075
|
+
const { port, host, clientDir, serverDir, rscEntryPath, compress: compressOption, purpose, silent } = options;
|
|
1012
1076
|
const prerenderSecret = readPrerenderSecret(serverDir);
|
|
1077
|
+
const compress = compressOption && readServerCompress(serverDir);
|
|
1013
1078
|
const rscModule = await importServerEntryModule(rscEntryPath);
|
|
1014
1079
|
const rscEntryRequire = createServerEntryRequire(rscEntryPath);
|
|
1015
1080
|
const rscHandler = resolveAppRouterHandler(rscModule.default);
|
|
@@ -1048,6 +1113,7 @@ async function startAppRouterServer(options) {
|
|
|
1048
1113
|
const handleRequestImpl = async (req, res) => {
|
|
1049
1114
|
const rawUrl = req.url ?? "/";
|
|
1050
1115
|
const rawPathname = rawUrl.split("?")[0];
|
|
1116
|
+
if (sendRepeatedSlashRedirect(rawUrl, res)) return;
|
|
1051
1117
|
if (isOpenRedirectShaped(rawPathname)) {
|
|
1052
1118
|
res.writeHead(404);
|
|
1053
1119
|
res.end("This page could not be found");
|
|
@@ -1095,14 +1161,23 @@ async function startAppRouterServer(options) {
|
|
|
1095
1161
|
"X-Content-Type-Options": "nosniff",
|
|
1096
1162
|
"Content-Disposition": imageConfig?.contentDispositionType === "attachment" ? "attachment" : "inline"
|
|
1097
1163
|
};
|
|
1098
|
-
if (await tryServeStatic(req, res, clientDir, params.imageUrl, false, staticCache, imageSecurityHeaders)) return;
|
|
1164
|
+
if (await tryServeStatic(req, res, clientDir, params.imageUrl, false, staticCache, imageSecurityHeaders, void 0, IMAGE_REPRESENTATION_HEADERS)) return;
|
|
1099
1165
|
res.writeHead(404);
|
|
1100
1166
|
res.end("Image not found");
|
|
1101
1167
|
return;
|
|
1102
1168
|
}
|
|
1103
1169
|
try {
|
|
1104
|
-
const request = nodeToWebRequest(req, rawUrl, prerenderSecret, appRouterI18nConfig, appRouterAuthorizeOnDemandRevalidate);
|
|
1105
|
-
const
|
|
1170
|
+
const request = nodeToWebRequest(req, rawUrl, prerenderSecret, appRouterI18nConfig, appRouterAuthorizeOnDemandRevalidate, signalFromNodeResponse(res));
|
|
1171
|
+
const ctx = createNodeExecutionContext(resolveTrustedNodeRevalidateOrigin(req, host, port));
|
|
1172
|
+
const recorded = {};
|
|
1173
|
+
if (purpose === "prerender") ctx.recordPrerenderSpecialError = (marker) => {
|
|
1174
|
+
recorded.marker = marker;
|
|
1175
|
+
};
|
|
1176
|
+
const response = await rscHandler(request, ctx);
|
|
1177
|
+
if (request.signal.aborted) {
|
|
1178
|
+
cancelResponseBody(response);
|
|
1179
|
+
return;
|
|
1180
|
+
}
|
|
1106
1181
|
const staticFileSignal = readStaticFileSignal(response);
|
|
1107
1182
|
if (staticFileSignal) {
|
|
1108
1183
|
let staticFilePath = "/";
|
|
@@ -1118,7 +1193,7 @@ async function startAppRouterServer(options) {
|
|
|
1118
1193
|
await sendWebResponse(notFoundResponse({ headers: toWebHeaders(staticResponseHeaders) }), req, res, compress);
|
|
1119
1194
|
return;
|
|
1120
1195
|
}
|
|
1121
|
-
await sendWebResponse(response, req, res, compress);
|
|
1196
|
+
await sendWebResponse(purpose === "prerender" ? markPrerenderSpecialError(response, recorded.marker) : response, req, res, compress);
|
|
1122
1197
|
} catch (e) {
|
|
1123
1198
|
console.error("[vinext] Server error:", e);
|
|
1124
1199
|
if (!res.headersSent) {
|
|
@@ -1149,7 +1224,7 @@ async function startAppRouterServer(options) {
|
|
|
1149
1224
|
},
|
|
1150
1225
|
getStatus: () => res.statusCode,
|
|
1151
1226
|
headers,
|
|
1152
|
-
isRsc:
|
|
1227
|
+
isRsc: target.split("?", 1)[0].endsWith(".rsc") || headers.get("RSC") === "1",
|
|
1153
1228
|
method: req.method ?? "GET",
|
|
1154
1229
|
target
|
|
1155
1230
|
});
|
|
@@ -1195,7 +1270,7 @@ function readPagesServerEntryPageRoutes(value) {
|
|
|
1195
1270
|
* - vinextConfig — embedded next.config.js settings
|
|
1196
1271
|
*/
|
|
1197
1272
|
async function startPagesRouterServer(options) {
|
|
1198
|
-
const { port, host, clientDir, serverEntryPath, compress, purpose, silent } = options;
|
|
1273
|
+
const { port, host, clientDir, serverDir, serverEntryPath, compress: compressOption, purpose, silent } = options;
|
|
1199
1274
|
const serverEntry = await importServerEntryModule(serverEntryPath);
|
|
1200
1275
|
const serverEntryRequire = createServerEntryRequire(serverEntryPath);
|
|
1201
1276
|
const { renderPage, handleApiRoute: handleApi, authorizeOnDemandRevalidate, runMiddleware, vinextConfig, buildId: pagesBuildId } = serverEntry;
|
|
@@ -1205,6 +1280,7 @@ async function startPagesRouterServer(options) {
|
|
|
1205
1280
|
const hasMiddleware = serverEntry.hasMiddleware === true;
|
|
1206
1281
|
const pageRoutes = readPagesServerEntryPageRoutes(serverEntry.pageRoutes);
|
|
1207
1282
|
const prerenderSecret = readPrerenderSecret(path.dirname(serverEntryPath));
|
|
1283
|
+
const compress = compressOption && readServerCompress(serverDir);
|
|
1208
1284
|
const basePath = vinextConfig?.basePath ?? "";
|
|
1209
1285
|
const assetPrefix = vinextConfig?.assetPrefix ?? "";
|
|
1210
1286
|
const pagesAssetPathPrefix = assetPrefixPathname(assetPrefix);
|
|
@@ -1261,6 +1337,7 @@ async function startPagesRouterServer(options) {
|
|
|
1261
1337
|
const handleRequestImpl = async (req, res) => {
|
|
1262
1338
|
const rawUrl = req.url ?? "/";
|
|
1263
1339
|
const rawPagesPathnameBeforeNormalize = rawUrl.split("?")[0];
|
|
1340
|
+
if (sendRepeatedSlashRedirect(rawUrl, res)) return;
|
|
1264
1341
|
if (isOpenRedirectShaped(rawPagesPathnameBeforeNormalize)) {
|
|
1265
1342
|
res.writeHead(404);
|
|
1266
1343
|
res.end("This page could not be found");
|
|
@@ -1366,10 +1443,12 @@ async function startPagesRouterServer(options) {
|
|
|
1366
1443
|
const reqHeaders = filterInternalHeaders(rawReqHeaders);
|
|
1367
1444
|
if (revalidationHostname) reqHeaders.set("host", revalidationHostname);
|
|
1368
1445
|
const method = req.method ?? "GET";
|
|
1369
|
-
const
|
|
1446
|
+
const signal = signalFromNodeResponse(res);
|
|
1447
|
+
const hasBody = method !== "GET" && method !== "HEAD" && !signal.aborted;
|
|
1370
1448
|
const webRequest = new Request(`${protocol}://${revalidationHostname ?? hostHeader}${url}`, {
|
|
1371
1449
|
method,
|
|
1372
1450
|
headers: reqHeaders,
|
|
1451
|
+
signal,
|
|
1373
1452
|
body: hasBody ? readNodeStream(req) : void 0,
|
|
1374
1453
|
duplex: hasBody ? "half" : void 0
|
|
1375
1454
|
});
|
|
@@ -1421,7 +1500,7 @@ async function startPagesRouterServer(options) {
|
|
|
1421
1500
|
"X-Content-Type-Options": "nosniff",
|
|
1422
1501
|
"Content-Disposition": pagesImageConfig?.contentDispositionType === "attachment" ? "attachment" : "inline"
|
|
1423
1502
|
};
|
|
1424
|
-
if (await tryServeStatic(req, res, clientDir, params.imageUrl, false, staticCache, imageSecurityHeaders)) return true;
|
|
1503
|
+
if (await tryServeStatic(req, res, clientDir, params.imageUrl, false, staticCache, imageSecurityHeaders, void 0, IMAGE_REPRESENTATION_HEADERS)) return true;
|
|
1425
1504
|
res.writeHead(404);
|
|
1426
1505
|
res.end("Image not found");
|
|
1427
1506
|
return true;
|
|
@@ -1434,13 +1513,18 @@ async function startPagesRouterServer(options) {
|
|
|
1434
1513
|
if (result.type === "handled") return;
|
|
1435
1514
|
if (result.type === "response") {
|
|
1436
1515
|
const { response } = result;
|
|
1516
|
+
if (webRequest.signal.aborted) {
|
|
1517
|
+
cancelResponseBody(response);
|
|
1518
|
+
return;
|
|
1519
|
+
}
|
|
1437
1520
|
const streamedApi = isVinextStreamedApiResponse(response);
|
|
1438
1521
|
if (isVinextStreamedHtmlResponse(response) || streamedApi || !response.body || result.defaultContentType === void 0) {
|
|
1439
1522
|
if (streamedApi && result.defaultContentType !== void 0 && !response.headers.has("content-type")) response.headers.set("content-type", result.defaultContentType);
|
|
1440
1523
|
await sendWebResponse(response, req, res, compress);
|
|
1441
1524
|
return;
|
|
1442
1525
|
}
|
|
1443
|
-
const responseBody =
|
|
1526
|
+
const responseBody = await bufferResponseBodyUntilAborted(response, webRequest.signal);
|
|
1527
|
+
if (!responseBody) return;
|
|
1444
1528
|
const ct = response.headers.get("content-type") ?? result.defaultContentType;
|
|
1445
1529
|
const responseHeaders = {};
|
|
1446
1530
|
response.headers.forEach((v, k) => {
|
|
@@ -1482,4 +1566,4 @@ async function startPagesRouterServer(options) {
|
|
|
1482
1566
|
};
|
|
1483
1567
|
}
|
|
1484
1568
|
//#endregion
|
|
1485
|
-
export {
|
|
1569
|
+
export { COMPRESS_THRESHOLD, importServerEntryModule, isCompressibleContentType, mergeResponseHeaders, mergeWebResponse, negotiateEncoding, nodeToWebRequest, readNodeStream, rememberCurrentServerEntryImportMtime, resolveAppRouterAssetPath, resolveAppRouterPrerenderSeeder, resolveRequestHost as resolveHost, resolveServerEntryImportUrl, sendCompressed, sendWebResponse, startProdServer, trustProxy, trustedHosts, tryServeStatic, waitForNodeResponseCompletion };
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { INTERNAL_HEADERS, VINEXT_INTERNAL_HEADERS } from "./headers.js";
|
|
2
2
|
import { hasBasePath, stripBasePath } from "../utils/base-path.js";
|
|
3
3
|
import { StaticFileSignalContext, createStaticFileSignal } from "./static-file-signal.js";
|
|
4
|
-
import { isOpenRedirectShaped } from "./open-redirect.js";
|
|
4
|
+
import { getRepeatedSlashRedirect, isOpenRedirectShaped, sendRepeatedSlashRedirect } from "./open-redirect.js";
|
|
5
5
|
//#region src/server/request-pipeline.d.ts
|
|
6
6
|
/**
|
|
7
7
|
* Apply the URL Standard's pathname canonicalization without decoding and
|
|
@@ -39,10 +39,21 @@ export declare function canonicalizeRequestUrlPathname(url: string): string;
|
|
|
39
39
|
* Backslashes are equivalent to forward slashes in the URL spec
|
|
40
40
|
* (e.g. `/\evil.com` is treated as `//evil.com` by browsers).
|
|
41
41
|
*
|
|
42
|
-
* Next.js
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
42
|
+
* Like Next.js, any raw path containing a literal backslash or a repeated
|
|
43
|
+
* slash first gets a 308 to the collapsed path (`//evil.com` → `/evil.com`,
|
|
44
|
+
* `//` → `/`); see `getRepeatedSlashRedirect`. We check the RAW
|
|
45
|
+
* pathname before normalization so this fires before normalizePath collapses
|
|
46
|
+
* `//`.
|
|
47
|
+
*
|
|
48
|
+
* Request-based (Worker) entries only see `request.url` after WHATWG URL
|
|
49
|
+
* parsing, which has already turned `\` into `/`; the raw request target is
|
|
50
|
+
* not available there. `/\evil.com` still redirects (it arrives as
|
|
51
|
+
* `//evil.com`), but a lone backslash such as `/foo\bar` arrives as
|
|
52
|
+
* `/foo/bar` and is routed without the redirect. On Cloudflare, requests that
|
|
53
|
+
* match a static asset are also answered by the asset layer before the
|
|
54
|
+
* Worker runs. Node entries (dev and `vinext start`) check the raw target.
|
|
55
|
+
*
|
|
56
|
+
* Percent-encoded variants get a 404, as in Next.js, because:
|
|
46
57
|
* - `%5C` decodes to `\` (browsers treat `/\evil.com` as `//evil.com`).
|
|
47
58
|
* - `%2F` decodes to `/` (so `/%2F/evil.com` effectively becomes `//evil.com`).
|
|
48
59
|
* These forms survive segment-wise decoding that re-encodes path delimiters
|
|
@@ -51,9 +62,10 @@ export declare function canonicalizeRequestUrlPathname(url: string): string;
|
|
|
51
62
|
* `isOpenRedirectShaped` for the full list of rejected leading-segment forms.
|
|
52
63
|
*
|
|
53
64
|
* @param rawPathname - The raw pathname from the URL, before any normalization
|
|
54
|
-
* @
|
|
65
|
+
* @param search - The raw query string (including `?`), kept in the 308 Location
|
|
66
|
+
* @returns A 308 or 404 Response, or null to continue
|
|
55
67
|
*/
|
|
56
|
-
export declare function guardProtocolRelativeUrl(rawPathname: string): Response | null;
|
|
68
|
+
export declare function guardProtocolRelativeUrl(rawPathname: string, search?: string): Response | null;
|
|
57
69
|
export type HeaderRecord = Record<string, string | string[]>;
|
|
58
70
|
type ResolvePublicFileRouteOptions = {
|
|
59
71
|
cleanPathname: string;
|
|
@@ -175,4 +187,4 @@ export declare function cloneRequestWithHeaders(request: Request, headers: Heade
|
|
|
175
187
|
*/
|
|
176
188
|
export declare function cloneRequestWithUrl(request: Request, url: string): Request;
|
|
177
189
|
//#endregion
|
|
178
|
-
export { INTERNAL_HEADERS, VINEXT_INTERNAL_HEADERS, createStaticFileSignal, hasBasePath, isOpenRedirectShaped, stripBasePath };
|
|
190
|
+
export { INTERNAL_HEADERS, VINEXT_INTERNAL_HEADERS, createStaticFileSignal, getRepeatedSlashRedirect, hasBasePath, isOpenRedirectShaped, sendRepeatedSlashRedirect, stripBasePath };
|
|
@@ -3,7 +3,7 @@ import { getUnconsumedMiddlewareRequestHeaders } from "../utils/middleware-reque
|
|
|
3
3
|
import { hasBasePath, removeTrailingSlash, stripBasePath } from "../utils/base-path.js";
|
|
4
4
|
import { INTERNAL_HEADERS, VINEXT_INTERNAL_HEADERS } from "./headers.js";
|
|
5
5
|
import { forbiddenResponse, methodNotAllowedResponse, notFoundResponse } from "./http-error-responses.js";
|
|
6
|
-
import { isOpenRedirectShaped } from "./open-redirect.js";
|
|
6
|
+
import { getRepeatedSlashRedirect, isOpenRedirectShaped, repeatedSlashRedirectResponse, sendRepeatedSlashRedirect } from "./open-redirect.js";
|
|
7
7
|
import { createStaticFileSignal } from "./static-file-signal.js";
|
|
8
8
|
//#region src/server/request-pipeline.ts
|
|
9
9
|
const PATHNAME_CANONICALIZATION_BASE = new URL("http://vinext.invalid/");
|
|
@@ -52,10 +52,21 @@ function canonicalizeRequestUrlPathname(url) {
|
|
|
52
52
|
* Backslashes are equivalent to forward slashes in the URL spec
|
|
53
53
|
* (e.g. `/\evil.com` is treated as `//evil.com` by browsers).
|
|
54
54
|
*
|
|
55
|
-
* Next.js
|
|
56
|
-
*
|
|
55
|
+
* Like Next.js, any raw path containing a literal backslash or a repeated
|
|
56
|
+
* slash first gets a 308 to the collapsed path (`//evil.com` → `/evil.com`,
|
|
57
|
+
* `//` → `/`); see `getRepeatedSlashRedirect`. We check the RAW
|
|
58
|
+
* pathname before normalization so this fires before normalizePath collapses
|
|
59
|
+
* `//`.
|
|
57
60
|
*
|
|
58
|
-
*
|
|
61
|
+
* Request-based (Worker) entries only see `request.url` after WHATWG URL
|
|
62
|
+
* parsing, which has already turned `\` into `/`; the raw request target is
|
|
63
|
+
* not available there. `/\evil.com` still redirects (it arrives as
|
|
64
|
+
* `//evil.com`), but a lone backslash such as `/foo\bar` arrives as
|
|
65
|
+
* `/foo/bar` and is routed without the redirect. On Cloudflare, requests that
|
|
66
|
+
* match a static asset are also answered by the asset layer before the
|
|
67
|
+
* Worker runs. Node entries (dev and `vinext start`) check the raw target.
|
|
68
|
+
*
|
|
69
|
+
* Percent-encoded variants get a 404, as in Next.js, because:
|
|
59
70
|
* - `%5C` decodes to `\` (browsers treat `/\evil.com` as `//evil.com`).
|
|
60
71
|
* - `%2F` decodes to `/` (so `/%2F/evil.com` effectively becomes `//evil.com`).
|
|
61
72
|
* These forms survive segment-wise decoding that re-encodes path delimiters
|
|
@@ -64,9 +75,12 @@ function canonicalizeRequestUrlPathname(url) {
|
|
|
64
75
|
* `isOpenRedirectShaped` for the full list of rejected leading-segment forms.
|
|
65
76
|
*
|
|
66
77
|
* @param rawPathname - The raw pathname from the URL, before any normalization
|
|
67
|
-
* @
|
|
78
|
+
* @param search - The raw query string (including `?`), kept in the 308 Location
|
|
79
|
+
* @returns A 308 or 404 Response, or null to continue
|
|
68
80
|
*/
|
|
69
|
-
function guardProtocolRelativeUrl(rawPathname) {
|
|
81
|
+
function guardProtocolRelativeUrl(rawPathname, search = "") {
|
|
82
|
+
const slashRedirect = repeatedSlashRedirectResponse(rawPathname + search);
|
|
83
|
+
if (slashRedirect) return slashRedirect;
|
|
70
84
|
if (isOpenRedirectShaped(rawPathname)) return notFoundResponse();
|
|
71
85
|
return null;
|
|
72
86
|
}
|
|
@@ -418,4 +432,4 @@ function cloneRequestWithUrl(request, url) {
|
|
|
418
432
|
return attachRequestCfMetadata(cloned, request);
|
|
419
433
|
}
|
|
420
434
|
//#endregion
|
|
421
|
-
export { INTERNAL_HEADERS, VINEXT_INTERNAL_HEADERS, attachRequestCfMetadata, canonicalizeRequestPathname, canonicalizeRequestUrlPathname, cloneRequestWithHeaders, cloneRequestWithUrl, createStaticFileSignal, filterInternalHeaders, guardProtocolRelativeUrl, hasBasePath, isOpenRedirectShaped, isOriginAllowed, normalizeTrailingSlash, normalizeTrailingSlashPathname, processMiddlewareHeaders, resolvePublicFileRoute, stripBasePath, validateCsrfOrigin, validateServerActionPayload };
|
|
435
|
+
export { INTERNAL_HEADERS, VINEXT_INTERNAL_HEADERS, attachRequestCfMetadata, canonicalizeRequestPathname, canonicalizeRequestUrlPathname, cloneRequestWithHeaders, cloneRequestWithUrl, createStaticFileSignal, filterInternalHeaders, getRepeatedSlashRedirect, guardProtocolRelativeUrl, hasBasePath, isOpenRedirectShaped, isOriginAllowed, normalizeTrailingSlash, normalizeTrailingSlashPathname, processMiddlewareHeaders, resolvePublicFileRoute, sendRepeatedSlashRedirect, stripBasePath, validateCsrfOrigin, validateServerActionPayload };
|