vinext 0.1.4 → 0.1.6
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 +2 -2
- package/dist/build/css-url-assets.d.ts +1 -1
- package/dist/build/css-url-assets.js +9 -7
- package/dist/build/google-fonts/find-font-files-in-css.d.ts +16 -0
- package/dist/build/google-fonts/find-font-files-in-css.js +28 -0
- package/dist/build/prerender.js +2 -1
- package/dist/build/report.d.ts +1 -2
- package/dist/build/report.js +2 -11
- package/dist/build/run-prerender.js +1 -1
- package/dist/cache/cache-adapters-virtual.js +1 -1
- package/dist/check.js +3 -3
- package/dist/client/pages-router-link-navigation.d.ts +10 -0
- package/dist/client/pages-router-link-navigation.js +3 -3
- package/dist/cloudflare/src/cache/kv-data-adapter.runtime.d.ts +1 -1
- package/dist/deploy.js +3 -3
- package/dist/entries/app-rsc-entry.js +25 -20
- package/dist/entries/app-rsc-manifest.js +76 -46
- package/dist/entries/pages-server-entry.js +3 -1
- package/dist/index.js +254 -198
- package/dist/init.d.ts +5 -4
- package/dist/init.js +27 -8
- package/dist/plugins/css-data-url.js +30 -26
- package/dist/plugins/extensionless-dynamic-import.js +27 -24
- package/dist/plugins/fonts.js +3 -1
- package/dist/plugins/import-meta-url.js +21 -15
- package/dist/plugins/instrumentation-client.js +1 -1
- package/dist/plugins/middleware-export-validation.d.ts +5 -0
- package/dist/plugins/middleware-export-validation.js +36 -0
- package/dist/plugins/middleware-server-only.js +7 -6
- package/dist/plugins/og-asset-ownership.d.ts +27 -0
- package/dist/plugins/og-asset-ownership.js +285 -0
- package/dist/plugins/og-assets.js +66 -46
- package/dist/plugins/optimize-imports.js +9 -3
- package/dist/plugins/remove-console.d.ts +7 -1
- package/dist/plugins/remove-console.js +4 -1
- package/dist/plugins/require-context.js +21 -20
- package/dist/plugins/strip-server-exports.d.ts +7 -1
- package/dist/plugins/strip-server-exports.js +4 -1
- package/dist/server/accept-encoding.d.ts +29 -0
- package/dist/server/accept-encoding.js +95 -0
- package/dist/server/app-bfcache-identity.d.ts +26 -0
- package/dist/server/app-bfcache-identity.js +127 -0
- package/dist/server/app-browser-entry.js +17 -11
- package/dist/server/app-browser-rsc-redirect.d.ts +2 -1
- package/dist/server/app-browser-rsc-redirect.js +8 -1
- package/dist/server/app-browser-state.d.ts +2 -21
- package/dist/server/app-browser-state.js +4 -128
- package/dist/server/app-browser-stream.js +1 -1
- package/dist/server/app-browser-visible-commit.js +3 -2
- package/dist/server/app-fallback-renderer.d.ts +1 -1
- package/dist/server/app-layout-param-observation.d.ts +1 -1
- package/dist/server/app-layout-param-observation.js +1 -1
- package/dist/server/app-middleware.d.ts +1 -0
- package/dist/server/app-middleware.js +3 -1
- package/dist/server/app-page-boundary-render.d.ts +1 -1
- package/dist/server/app-page-boundary.js +1 -1
- package/dist/server/app-page-cache-finalizer.d.ts +62 -0
- package/dist/server/app-page-cache-finalizer.js +122 -0
- package/dist/server/app-page-cache-render.d.ts +2 -2
- package/dist/server/app-page-cache-render.js +1 -1
- package/dist/server/app-page-cache.d.ts +2 -53
- package/dist/server/app-page-cache.js +5 -131
- package/dist/server/app-page-dispatch.d.ts +4 -4
- package/dist/server/app-page-dispatch.js +10 -8
- package/dist/server/app-page-head.js +2 -2
- package/dist/server/app-page-probe.js +3 -2
- package/dist/server/app-page-render-observation.js +2 -2
- package/dist/server/app-page-render.d.ts +3 -3
- package/dist/server/app-page-render.js +3 -2
- package/dist/server/app-page-request.d.ts +7 -0
- package/dist/server/app-page-request.js +19 -1
- package/dist/server/app-page-stream.d.ts +2 -9
- package/dist/server/app-page-stream.js +1 -35
- package/dist/server/app-prerender-endpoints.d.ts +3 -3
- package/dist/server/app-prerender-endpoints.js +3 -4
- package/dist/server/app-request-context.d.ts +1 -2
- package/dist/server/app-request-context.js +2 -1
- package/dist/server/app-route-handler-dispatch.js +3 -2
- package/dist/server/app-route-handler-execution.d.ts +1 -1
- package/dist/server/app-route-handler-execution.js +1 -1
- package/dist/server/app-route-handler-response.d.ts +1 -1
- package/dist/server/app-route-module-loader.d.ts +62 -14
- package/dist/server/app-route-module-loader.js +61 -10
- package/dist/server/app-router-entry.js +2 -1
- package/dist/server/app-rsc-handler.d.ts +7 -6
- package/dist/server/app-rsc-handler.js +51 -40
- package/dist/server/app-rsc-response-finalizer.js +1 -1
- package/dist/server/app-rsc-route-matching.d.ts +9 -0
- package/dist/server/app-rsc-route-matching.js +13 -0
- package/dist/server/app-server-action-execution.d.ts +1 -1
- package/dist/server/app-server-action-execution.js +5 -4
- package/dist/server/app-ssr-entry.d.ts +1 -1
- package/dist/server/app-ssr-entry.js +9 -7
- package/dist/server/app-ssr-router-instance.d.ts +6 -0
- package/dist/server/app-ssr-router-instance.js +24 -0
- package/dist/server/app-ssr-stream.js +1 -1
- package/dist/server/artifact-compatibility.js +1 -1
- package/dist/server/client-reuse-manifest.js +1 -1
- package/dist/server/defer-until-stream-consumed.d.ts +7 -0
- package/dist/server/defer-until-stream-consumed.js +34 -0
- package/dist/server/dev-server.js +1 -1
- package/dist/server/headers.d.ts +5 -1
- package/dist/server/headers.js +5 -1
- package/dist/server/instrumentation.js +1 -1
- package/dist/server/isr-cache.d.ts +1 -1
- package/dist/server/isr-cache.js +1 -1
- package/dist/server/isr-decision.d.ts +1 -1
- package/dist/server/metadata-route-response.d.ts +3 -3
- package/dist/server/middleware-matcher.js +8 -6
- package/dist/server/middleware-runtime.d.ts +2 -1
- package/dist/server/middleware-runtime.js +21 -10
- package/dist/server/middleware.d.ts +2 -2
- package/dist/server/middleware.js +26 -11
- package/dist/server/open-redirect.d.ts +12 -0
- package/dist/server/open-redirect.js +21 -0
- package/dist/server/pages-page-data.d.ts +1 -1
- package/dist/server/pages-page-response.d.ts +1 -1
- package/dist/server/pages-page-response.js +2 -2
- package/dist/server/pages-request-pipeline.d.ts +3 -1
- package/dist/server/pages-request-pipeline.js +4 -2
- package/dist/server/prod-server.d.ts +1 -1
- package/dist/server/prod-server.js +87 -73
- package/dist/server/request-pipeline.d.ts +1 -24
- package/dist/server/request-pipeline.js +1 -33
- package/dist/server/seed-cache.d.ts +1 -1
- package/dist/shims/cache-handler.d.ts +106 -0
- package/dist/shims/cache-handler.js +176 -0
- package/dist/shims/cache-request-state.d.ts +47 -0
- package/dist/shims/cache-request-state.js +126 -0
- package/dist/shims/cache-runtime.d.ts +2 -2
- package/dist/shims/cache-runtime.js +3 -14
- package/dist/shims/cache.d.ts +3 -231
- package/dist/shims/cache.js +17 -383
- package/dist/shims/cdn-cache.d.ts +1 -1
- package/dist/shims/cdn-cache.js +1 -1
- package/dist/shims/error-boundary-navigation.d.ts +7 -0
- package/dist/shims/error-boundary-navigation.js +44 -0
- package/dist/shims/error-boundary.js +10 -8
- package/dist/shims/error.js +2 -1
- package/dist/shims/fetch-cache.js +1 -1
- package/dist/shims/font-google-base.d.ts +1 -0
- package/dist/shims/font-google-base.js +6 -21
- package/dist/shims/form.js +1 -1
- package/dist/shims/image.js +67 -9
- package/dist/shims/internal/app-page-props-cache-key.d.ts +5 -0
- package/dist/shims/internal/app-page-props-cache-key.js +16 -0
- package/dist/shims/internal/interpolate-as.d.ts +25 -0
- package/dist/shims/internal/interpolate-as.js +196 -0
- package/dist/shims/internal/navigation-untracked.js +2 -1
- package/dist/shims/layout-segment-context.d.ts +1 -1
- package/dist/shims/layout-segment-context.js +2 -1
- package/dist/shims/link.js +26 -3
- package/dist/shims/navigation-context-state.d.ts +40 -0
- package/dist/shims/navigation-context-state.js +116 -0
- package/dist/shims/navigation-errors.d.ts +55 -0
- package/dist/shims/navigation-errors.js +110 -0
- package/dist/shims/navigation-server.d.ts +3 -0
- package/dist/shims/navigation-server.js +3 -0
- package/dist/shims/navigation-state.d.ts +1 -2
- package/dist/shims/navigation-state.js +2 -1
- package/dist/shims/navigation.d.ts +3 -291
- package/dist/shims/navigation.js +13 -444
- package/dist/shims/navigation.react-server.d.ts +2 -2
- package/dist/shims/navigation.react-server.js +4 -2
- package/dist/shims/request-state-types.d.ts +3 -3
- package/dist/shims/router.d.ts +1 -3
- package/dist/shims/router.js +62 -18
- package/dist/shims/script.js +1 -1
- package/dist/shims/slot.js +3 -1
- package/dist/shims/unified-request-context.d.ts +2 -2
- package/dist/typegen.js +2 -10
- package/dist/utils/project.d.ts +7 -1
- package/dist/utils/project.js +16 -2
- package/dist/utils/virtual-module.d.ts +5 -0
- package/dist/utils/virtual-module.js +0 -0
- package/package.json +7 -3
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { getRequestExecutionContext } from "../shims/request-context.js";
|
|
2
2
|
import { reportRequestError } from "./instrumentation.js";
|
|
3
3
|
import { setCacheStateHeaders } from "./cache-headers.js";
|
|
4
|
-
import { fnv1a52 } from "../utils/hash.js";
|
|
5
|
-
import { encodeCacheTag } from "../utils/encode-cache-tag.js";
|
|
6
4
|
import { NEVER_CACHE_CONTROL, NO_STORE_CACHE_CONTROL, applyCdnResponseHeaders } from "./cache-control.js";
|
|
7
5
|
import { buildMissIsrCacheControl } from "./isr-decision.js";
|
|
6
|
+
import { fnv1a52 } from "../utils/hash.js";
|
|
7
|
+
import { encodeCacheTag } from "../utils/encode-cache-tag.js";
|
|
8
8
|
import { appendAssetDeploymentIdQuery } from "../utils/deployment-id.js";
|
|
9
9
|
import { withScriptNonce } from "../shims/script-nonce-context.js";
|
|
10
10
|
import { createNonceAttribute, escapeHtmlAttr } from "./html.js";
|
|
@@ -106,7 +106,9 @@ type PagesPipelineResult = {
|
|
|
106
106
|
* ASSUMPTION: request already has internal headers filtered and basePath stripped.
|
|
107
107
|
* The adapter is responsible for that pre-processing before calling runPagesRequest.
|
|
108
108
|
* The adapter also handles: open-redirect guard, _next/static 404, image optimization,
|
|
109
|
-
* _next/data normalization
|
|
109
|
+
* _next/data normalization and classification: adapters must rewrite the data
|
|
110
|
+
* URL to its page pathname and set `isDataReq` (the source of truth here), Node
|
|
111
|
+
* decode/normalize/400, public-file serving.
|
|
110
112
|
* runPagesRequest receives a "clean" request with basePath-stripped URL.
|
|
111
113
|
*/
|
|
112
114
|
declare function runPagesRequest(request: Request, deps: PagesPipelineDeps): Promise<PagesPipelineResult>;
|
|
@@ -40,7 +40,9 @@ function wrapMiddlewareWithBasePath(runMiddleware, basePath, hadBasePath) {
|
|
|
40
40
|
* ASSUMPTION: request already has internal headers filtered and basePath stripped.
|
|
41
41
|
* The adapter is responsible for that pre-processing before calling runPagesRequest.
|
|
42
42
|
* The adapter also handles: open-redirect guard, _next/static 404, image optimization,
|
|
43
|
-
* _next/data normalization
|
|
43
|
+
* _next/data normalization and classification: adapters must rewrite the data
|
|
44
|
+
* URL to its page pathname and set `isDataReq` (the source of truth here), Node
|
|
45
|
+
* decode/normalize/400, public-file serving.
|
|
44
46
|
* runPagesRequest receives a "clean" request with basePath-stripped URL.
|
|
45
47
|
*/
|
|
46
48
|
async function runPagesRequest(request, deps) {
|
|
@@ -54,7 +56,7 @@ async function runPagesRequest(request, deps) {
|
|
|
54
56
|
hadBasePath
|
|
55
57
|
};
|
|
56
58
|
{
|
|
57
|
-
const trailingSlashRedirect = normalizeTrailingSlash(pathname, basePath, trailingSlash, search);
|
|
59
|
+
const trailingSlashRedirect = isDataReq ? null : normalizeTrailingSlash(pathname, basePath, trailingSlash, search);
|
|
58
60
|
if (trailingSlashRedirect) return {
|
|
59
61
|
type: "response",
|
|
60
62
|
response: trailingSlashRedirect
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { negotiateEncoding } from "./accept-encoding.js";
|
|
1
2
|
import { StaticFileCache } from "./static-file-cache.js";
|
|
2
3
|
import { resolveRequestHost, trustProxy, trustedHosts } from "./proxy-trust.js";
|
|
3
4
|
import { IncomingMessage, ServerResponse } from "node:http";
|
|
@@ -59,7 +60,6 @@ type ProdServerOptions = {
|
|
|
59
60
|
declare const COMPRESSIBLE_TYPES: Set<string>;
|
|
60
61
|
/** Minimum size threshold for compression (in bytes). Below this, compression overhead isn't worth it. */
|
|
61
62
|
declare const COMPRESS_THRESHOLD = 1024;
|
|
62
|
-
declare function negotiateEncoding(req: IncomingMessage): "zstd" | "br" | "gzip" | "deflate" | null;
|
|
63
63
|
/**
|
|
64
64
|
* Merge middleware headers and a Web Response's headers into a single
|
|
65
65
|
* record suitable for Node.js `res.writeHead()`. Uses `getSetCookie()`
|
|
@@ -3,7 +3,8 @@ import { hasBasePath, stripBasePath } from "../utils/base-path.js";
|
|
|
3
3
|
import { VINEXT_PRERENDER_ROUTE_PARAMS_HEADER, VINEXT_PRERENDER_SECRET_HEADER, VINEXT_STATIC_FILE_HEADER } from "./headers.js";
|
|
4
4
|
import { normalizePath } from "./normalize-path.js";
|
|
5
5
|
import { notFoundResponse } from "./http-error-responses.js";
|
|
6
|
-
import {
|
|
6
|
+
import { isOpenRedirectShaped } from "./open-redirect.js";
|
|
7
|
+
import { filterInternalHeaders } from "./request-pipeline.js";
|
|
7
8
|
import { isUnknownRecord } from "../utils/record.js";
|
|
8
9
|
import { resolveRequestHost, resolveRequestProtocol, trustProxy, trustedHosts } from "./proxy-trust.js";
|
|
9
10
|
import { DEFAULT_DEVICE_SIZES, DEFAULT_IMAGE_SIZES, isImageOptimizationPath, isSafeImageContentType, parseImageParams } from "./image-optimization.js";
|
|
@@ -18,6 +19,7 @@ import { readTrustedPrerenderRouteParamsFromHeaders, serializePrerenderRoutePara
|
|
|
18
19
|
import { computeClientRuntimeMetadata } from "../utils/client-runtime-metadata.js";
|
|
19
20
|
import { readPrerenderSecret } from "../build/server-manifest.js";
|
|
20
21
|
import { seedMemoryCacheFromPrerender } from "./seed-cache.js";
|
|
22
|
+
import { negotiateEncoding, parseAcceptedEncodings, selectContentEncoding } from "./accept-encoding.js";
|
|
21
23
|
import fs from "node:fs";
|
|
22
24
|
import path from "node:path";
|
|
23
25
|
import fs$1 from "node:fs/promises";
|
|
@@ -166,25 +168,6 @@ const COMPRESSIBLE_TYPES = new Set([
|
|
|
166
168
|
/** Minimum size threshold for compression (in bytes). Below this, compression overhead isn't worth it. */
|
|
167
169
|
const COMPRESS_THRESHOLD = 1024;
|
|
168
170
|
/**
|
|
169
|
-
* Parse the Accept-Encoding header and return the best supported encoding.
|
|
170
|
-
* Preference order: zstd > br > gzip > deflate > identity.
|
|
171
|
-
*
|
|
172
|
-
* zstd decompresses ~3-5x faster than brotli at similar compression ratios.
|
|
173
|
-
* Supported in Chrome 123+, Firefox 126+. Safari can decompress but doesn't
|
|
174
|
-
* send zstd in Accept-Encoding, so it transparently falls back to br/gzip.
|
|
175
|
-
*/
|
|
176
|
-
const HAS_ZSTD = typeof zlib.createZstdCompress === "function";
|
|
177
|
-
function negotiateEncoding(req) {
|
|
178
|
-
const accept = req.headers["accept-encoding"];
|
|
179
|
-
if (!accept || typeof accept !== "string") return null;
|
|
180
|
-
const lower = accept.toLowerCase();
|
|
181
|
-
if (HAS_ZSTD && lower.includes("zstd")) return "zstd";
|
|
182
|
-
if (lower.includes("br")) return "br";
|
|
183
|
-
if (lower.includes("gzip")) return "gzip";
|
|
184
|
-
if (lower.includes("deflate")) return "deflate";
|
|
185
|
-
return null;
|
|
186
|
-
}
|
|
187
|
-
/**
|
|
188
171
|
* Create a compression stream for the given encoding.
|
|
189
172
|
*/
|
|
190
173
|
function createCompressor(encoding, mode = "default") {
|
|
@@ -264,6 +247,23 @@ function omitHeadersCaseInsensitive(headersRecord, targets) {
|
|
|
264
247
|
}
|
|
265
248
|
return filtered;
|
|
266
249
|
}
|
|
250
|
+
function mergeVaryHeader(headers, value) {
|
|
251
|
+
const merged = { ...headers };
|
|
252
|
+
const existingKey = Object.keys(merged).find((key) => key.toLowerCase() === "vary");
|
|
253
|
+
if (!existingKey) {
|
|
254
|
+
merged.Vary = value;
|
|
255
|
+
return merged;
|
|
256
|
+
}
|
|
257
|
+
const rawVary = merged[existingKey];
|
|
258
|
+
const existingVary = Array.isArray(rawVary) ? rawVary.join(", ") : rawVary;
|
|
259
|
+
if (existingVary.trim().length === 0) {
|
|
260
|
+
merged[existingKey] = value;
|
|
261
|
+
return merged;
|
|
262
|
+
}
|
|
263
|
+
const values = existingVary.split(",").map((entry) => entry.trim().toLowerCase());
|
|
264
|
+
if (!values.includes("*") && !values.includes(value.toLowerCase())) merged[existingKey] = `${existingVary}, ${value}`;
|
|
265
|
+
return merged;
|
|
266
|
+
}
|
|
267
267
|
function matchesIfNoneMatchHeader(ifNoneMatch, etag) {
|
|
268
268
|
if (!ifNoneMatch) return false;
|
|
269
269
|
if (ifNoneMatch === "*") return true;
|
|
@@ -317,33 +317,37 @@ function mergeWebResponse(middlewareHeaders, response, statusOverride) {
|
|
|
317
317
|
function sendCompressed(req, res, body, contentType, statusCode, extraHeaders = {}, compress = true, statusText) {
|
|
318
318
|
const buf = typeof body === "string" ? Buffer.from(body) : body;
|
|
319
319
|
const baseType = contentType.split(";")[0].trim();
|
|
320
|
-
const
|
|
320
|
+
const varyByEncoding = compress && COMPRESSIBLE_TYPES.has(baseType);
|
|
321
|
+
const encoding = compress ? negotiateEncoding(req) : "identity";
|
|
321
322
|
const headersWithoutBodyHeaders = omitHeadersCaseInsensitive(extraHeaders, OMIT_BODY_HEADERS);
|
|
322
|
-
const writeHead = (headers) => {
|
|
323
|
-
if (
|
|
324
|
-
else res.writeHead(
|
|
323
|
+
const writeHead = (headers, responseStatus = statusCode, responseStatusText = statusText) => {
|
|
324
|
+
if (responseStatusText) res.writeHead(responseStatus, responseStatusText, headers);
|
|
325
|
+
else res.writeHead(responseStatus, headers);
|
|
325
326
|
};
|
|
326
|
-
if (encoding &&
|
|
327
|
-
|
|
328
|
-
const rawVary = extraHeaders["Vary"] ?? extraHeaders["vary"];
|
|
329
|
-
const existingVary = Array.isArray(rawVary) ? rawVary.join(", ") : rawVary;
|
|
330
|
-
let varyValue;
|
|
331
|
-
if (existingVary) varyValue = existingVary.toLowerCase().includes("accept-encoding") ? existingVary : existingVary + ", Accept-Encoding";
|
|
332
|
-
else varyValue = "Accept-Encoding";
|
|
333
|
-
writeHead({
|
|
327
|
+
if (encoding !== "identity" && varyByEncoding && buf.length >= 1024) {
|
|
328
|
+
writeHead(mergeVaryHeader({
|
|
334
329
|
...headersWithoutBodyHeaders,
|
|
335
330
|
"Content-Type": contentType,
|
|
336
|
-
"Content-Encoding": encoding
|
|
337
|
-
|
|
338
|
-
|
|
331
|
+
"Content-Encoding": encoding
|
|
332
|
+
}, "Accept-Encoding"));
|
|
333
|
+
if (req.method === "HEAD") {
|
|
334
|
+
res.end();
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
337
|
+
const compressor = createCompressor(encoding);
|
|
339
338
|
compressor.end(buf);
|
|
340
339
|
pipeline(compressor, res, () => {});
|
|
341
340
|
} else {
|
|
342
|
-
|
|
341
|
+
const identityHeaders = {
|
|
343
342
|
...headersWithoutBodyHeaders,
|
|
344
343
|
"Content-Type": contentType,
|
|
345
344
|
"Content-Length": String(buf.length)
|
|
346
|
-
}
|
|
345
|
+
};
|
|
346
|
+
writeHead(varyByEncoding ? mergeVaryHeader(identityHeaders, "Accept-Encoding") : identityHeaders);
|
|
347
|
+
if (req.method === "HEAD") {
|
|
348
|
+
res.end();
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
347
351
|
res.end(buf);
|
|
348
352
|
}
|
|
349
353
|
}
|
|
@@ -377,24 +381,35 @@ async function tryServeStatic(req, res, clientDir, pathname, compress, cache, ex
|
|
|
377
381
|
}
|
|
378
382
|
const entry = cache.lookup(lookupPath);
|
|
379
383
|
if (!entry) return false;
|
|
384
|
+
const rawAe = compress ? req.headers["accept-encoding"] : void 0;
|
|
385
|
+
const parsed = typeof rawAe === "string" ? parseAcceptedEncodings(rawAe) : void 0;
|
|
386
|
+
const availableVariants = [
|
|
387
|
+
...entry.zst ? ["zstd"] : [],
|
|
388
|
+
...entry.br ? ["br"] : [],
|
|
389
|
+
...entry.gz ? ["gzip"] : []
|
|
390
|
+
];
|
|
391
|
+
const variesByEncoding = compress && availableVariants.length > 0;
|
|
392
|
+
const selected = parsed ? selectContentEncoding(parsed, availableVariants) : "identity";
|
|
393
|
+
const variant = selected === "zstd" ? entry.zst : selected === "br" ? entry.br : selected === "gzip" ? entry.gz : entry.original;
|
|
380
394
|
const ifNoneMatch = req.headers["if-none-match"];
|
|
381
395
|
if (responseStatus === 200 && typeof ifNoneMatch === "string" && matchesIfNoneMatchHeader(ifNoneMatch, entry.etag)) {
|
|
382
|
-
|
|
396
|
+
const notModifiedHeaders = variesByEncoding ? mergeVaryHeader({
|
|
383
397
|
...entry.notModifiedHeaders,
|
|
384
398
|
...extraHeaders
|
|
385
|
-
})
|
|
386
|
-
|
|
399
|
+
}, "Accept-Encoding") : {
|
|
400
|
+
...entry.notModifiedHeaders,
|
|
401
|
+
...extraHeaders
|
|
402
|
+
};
|
|
403
|
+
if (selected !== "identity") notModifiedHeaders["Content-Encoding"] = selected;
|
|
404
|
+
res.writeHead(304, notModifiedHeaders);
|
|
387
405
|
res.end();
|
|
388
406
|
return true;
|
|
389
407
|
}
|
|
390
|
-
const
|
|
391
|
-
const ae = typeof rawAe === "string" ? rawAe.toLowerCase() : void 0;
|
|
392
|
-
const variant = ae ? ae.includes("zstd") && entry.zst || ae.includes("br") && entry.br || ae.includes("gzip") && entry.gz || entry.original : entry.original;
|
|
393
|
-
if (extraHeaders) res.writeHead(responseStatus, {
|
|
408
|
+
const responseHeaders = {
|
|
394
409
|
...variant.headers,
|
|
395
410
|
...extraHeaders
|
|
396
|
-
}
|
|
397
|
-
|
|
411
|
+
};
|
|
412
|
+
res.writeHead(responseStatus, variesByEncoding ? mergeVaryHeader(responseHeaders, "Accept-Encoding") : responseHeaders);
|
|
398
413
|
if (omitBody || req.method === "HEAD") {
|
|
399
414
|
res.end();
|
|
400
415
|
return true;
|
|
@@ -427,18 +442,6 @@ async function tryServeStatic(req, res, clientDir, pathname, compress, cache, ex
|
|
|
427
442
|
const etag = isHashed && etagFromFilenameHash(resolved.path, ext) || `W/"${resolved.size}-${Math.floor(resolved.mtimeMs / 1e3)}"`;
|
|
428
443
|
const baseType = ct.split(";")[0].trim();
|
|
429
444
|
const isCompressible = compress && COMPRESSIBLE_TYPES.has(baseType);
|
|
430
|
-
const ifNoneMatch = req.headers["if-none-match"];
|
|
431
|
-
if (responseStatus === 200 && typeof ifNoneMatch === "string" && matchesIfNoneMatchHeader(ifNoneMatch, etag)) {
|
|
432
|
-
const notModifiedHeaders = {
|
|
433
|
-
ETag: etag,
|
|
434
|
-
"Cache-Control": cacheControl,
|
|
435
|
-
...isCompressible ? { Vary: "Accept-Encoding" } : void 0,
|
|
436
|
-
...extraHeaders
|
|
437
|
-
};
|
|
438
|
-
res.writeHead(304, notModifiedHeaders);
|
|
439
|
-
res.end();
|
|
440
|
-
return true;
|
|
441
|
-
}
|
|
442
445
|
const baseHeaders = {
|
|
443
446
|
"Content-Type": ct,
|
|
444
447
|
"Cache-Control": cacheControl,
|
|
@@ -447,12 +450,19 @@ async function tryServeStatic(req, res, clientDir, pathname, compress, cache, ex
|
|
|
447
450
|
};
|
|
448
451
|
if (isCompressible) {
|
|
449
452
|
const encoding = negotiateEncoding(req);
|
|
450
|
-
|
|
451
|
-
|
|
453
|
+
const ifNoneMatch = req.headers["if-none-match"];
|
|
454
|
+
if (responseStatus === 200 && typeof ifNoneMatch === "string" && matchesIfNoneMatchHeader(ifNoneMatch, etag)) {
|
|
455
|
+
const notModifiedHeaders = mergeVaryHeader(baseHeaders, "Accept-Encoding");
|
|
456
|
+
if (encoding !== "identity") notModifiedHeaders["Content-Encoding"] = encoding;
|
|
457
|
+
res.writeHead(304, notModifiedHeaders);
|
|
458
|
+
res.end();
|
|
459
|
+
return true;
|
|
460
|
+
}
|
|
461
|
+
if (encoding !== "identity") {
|
|
462
|
+
res.writeHead(responseStatus, mergeVaryHeader({
|
|
452
463
|
...baseHeaders,
|
|
453
|
-
"Content-Encoding": encoding
|
|
454
|
-
|
|
455
|
-
});
|
|
464
|
+
"Content-Encoding": encoding
|
|
465
|
+
}, "Accept-Encoding"));
|
|
456
466
|
if (omitBody || req.method === "HEAD") {
|
|
457
467
|
res.end();
|
|
458
468
|
return true;
|
|
@@ -467,10 +477,17 @@ async function tryServeStatic(req, res, clientDir, pathname, compress, cache, ex
|
|
|
467
477
|
return true;
|
|
468
478
|
}
|
|
469
479
|
}
|
|
470
|
-
|
|
480
|
+
const ifNoneMatch = req.headers["if-none-match"];
|
|
481
|
+
if (responseStatus === 200 && typeof ifNoneMatch === "string" && matchesIfNoneMatchHeader(ifNoneMatch, etag)) {
|
|
482
|
+
res.writeHead(304, isCompressible ? mergeVaryHeader(baseHeaders, "Accept-Encoding") : baseHeaders);
|
|
483
|
+
res.end();
|
|
484
|
+
return true;
|
|
485
|
+
}
|
|
486
|
+
const identityHeaders = {
|
|
471
487
|
...baseHeaders,
|
|
472
488
|
"Content-Length": String(resolved.size)
|
|
473
|
-
}
|
|
489
|
+
};
|
|
490
|
+
res.writeHead(responseStatus, isCompressible ? mergeVaryHeader(identityHeaders, "Accept-Encoding") : identityHeaders);
|
|
474
491
|
if (omitBody || req.method === "HEAD") {
|
|
475
492
|
res.end();
|
|
476
493
|
return true;
|
|
@@ -567,25 +584,22 @@ async function sendWebResponse(webResponse, req, res, compress) {
|
|
|
567
584
|
if (existing !== void 0) nodeHeaders[key] = Array.isArray(existing) ? [...existing, value] : [existing, value];
|
|
568
585
|
else nodeHeaders[key] = value;
|
|
569
586
|
});
|
|
587
|
+
const alreadyEncoded = webResponse.headers.get("content-encoding") !== null;
|
|
570
588
|
if (!webResponse.body) {
|
|
571
589
|
writeHead(nodeHeaders);
|
|
572
590
|
res.end();
|
|
573
591
|
return;
|
|
574
592
|
}
|
|
575
|
-
const alreadyEncoded = webResponse.headers.has("content-encoding");
|
|
576
593
|
const baseType = (webResponse.headers.get("content-type") ?? "").split(";")[0].trim();
|
|
577
|
-
const
|
|
578
|
-
const
|
|
594
|
+
const varyByEncoding = compress && !alreadyEncoded && COMPRESSIBLE_TYPES.has(baseType);
|
|
595
|
+
const encoding = compress && !alreadyEncoded ? negotiateEncoding(req) : "identity";
|
|
596
|
+
const shouldCompress = encoding !== "identity" && COMPRESSIBLE_TYPES.has(baseType);
|
|
579
597
|
if (shouldCompress) {
|
|
580
598
|
delete nodeHeaders["content-length"];
|
|
581
599
|
delete nodeHeaders["Content-Length"];
|
|
582
600
|
nodeHeaders["Content-Encoding"] = encoding;
|
|
583
|
-
const existingVary = nodeHeaders["Vary"] ?? nodeHeaders["vary"];
|
|
584
|
-
if (existingVary) {
|
|
585
|
-
if (!String(existingVary).toLowerCase().includes("accept-encoding")) nodeHeaders["Vary"] = existingVary + ", Accept-Encoding";
|
|
586
|
-
} else nodeHeaders["Vary"] = "Accept-Encoding";
|
|
587
601
|
}
|
|
588
|
-
writeHead(nodeHeaders);
|
|
602
|
+
writeHead(varyByEncoding ? mergeVaryHeader(nodeHeaders, "Accept-Encoding") : nodeHeaders);
|
|
589
603
|
if (req.method === "HEAD") {
|
|
590
604
|
cancelResponseBody(webResponse);
|
|
591
605
|
res.end();
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { NextHeader } from "../config/next-config.js";
|
|
2
2
|
import { BasePathMatchState, RequestContext } from "../config/config-matchers.js";
|
|
3
3
|
import { INTERNAL_HEADERS, VINEXT_INTERNAL_HEADERS } from "./headers.js";
|
|
4
|
+
import { isOpenRedirectShaped } from "./open-redirect.js";
|
|
4
5
|
import { hasBasePath, stripBasePath } from "../utils/base-path.js";
|
|
5
6
|
|
|
6
7
|
//#region src/server/request-pipeline.d.ts
|
|
@@ -42,30 +43,6 @@ import { hasBasePath, stripBasePath } from "../utils/base-path.js";
|
|
|
42
43
|
* @returns A 404 Response if the path is protocol-relative, or null to continue
|
|
43
44
|
*/
|
|
44
45
|
declare function guardProtocolRelativeUrl(rawPathname: string): Response | null;
|
|
45
|
-
/**
|
|
46
|
-
* Returns true if a request pathname looks like a protocol-relative open
|
|
47
|
-
* redirect, in either literal or percent-encoded form.
|
|
48
|
-
*
|
|
49
|
-
* Exported for call sites that need to replicate the guard inline (Pages
|
|
50
|
-
* Router worker codegen, Node production server) and for defense-in-depth
|
|
51
|
-
* checks inside redirect emitters.
|
|
52
|
-
*
|
|
53
|
-
* A pathname is considered "open redirect shaped" when its first segment,
|
|
54
|
-
* after decoding backslashes and encoded delimiters, would cause a browser
|
|
55
|
-
* to resolve a `Location` containing the pathname as protocol-relative:
|
|
56
|
-
*
|
|
57
|
-
* - literal `//evil.com`
|
|
58
|
-
* - literal `/\evil.com` (browsers normalize `\` to `/`)
|
|
59
|
-
* - encoded `/%5Cevil.com` (`%5C` decodes to `\` in Location)
|
|
60
|
-
* - encoded `/%2F/evil.com` (`%2F` decodes to `/` → `//`)
|
|
61
|
-
* - mixed `/%5C%2F`, `/%5C%5C` (and other combinations)
|
|
62
|
-
*
|
|
63
|
-
* We explicitly do not require a valid percent sequence elsewhere in the
|
|
64
|
-
* pathname — we only examine the leading bytes (up to the second real or
|
|
65
|
-
* encoded delimiter) so malformed suffixes can still reach the normal
|
|
66
|
-
* "400 Bad Request" decode path instead of being masked as "404".
|
|
67
|
-
*/
|
|
68
|
-
declare function isOpenRedirectShaped(rawPathname: string): boolean;
|
|
69
46
|
type HeaderRecord = Record<string, string | string[]>;
|
|
70
47
|
type ApplyConfigHeadersOptions = {
|
|
71
48
|
configHeaders: NextHeader[];
|
|
@@ -2,6 +2,7 @@ import { hasBasePath, removeTrailingSlash, stripBasePath } from "../utils/base-p
|
|
|
2
2
|
import { INTERNAL_HEADERS, VINEXT_INTERNAL_HEADERS, VINEXT_STATIC_FILE_HEADER } from "./headers.js";
|
|
3
3
|
import { matchHeaders } from "../config/config-matchers.js";
|
|
4
4
|
import { forbiddenResponse, notFoundResponse } from "./http-error-responses.js";
|
|
5
|
+
import { isOpenRedirectShaped } from "./open-redirect.js";
|
|
5
6
|
//#region src/server/request-pipeline.ts
|
|
6
7
|
/**
|
|
7
8
|
* Shared request pipeline utilities.
|
|
@@ -44,39 +45,6 @@ function guardProtocolRelativeUrl(rawPathname) {
|
|
|
44
45
|
if (isOpenRedirectShaped(rawPathname)) return notFoundResponse();
|
|
45
46
|
return null;
|
|
46
47
|
}
|
|
47
|
-
/**
|
|
48
|
-
* Returns true if a request pathname looks like a protocol-relative open
|
|
49
|
-
* redirect, in either literal or percent-encoded form.
|
|
50
|
-
*
|
|
51
|
-
* Exported for call sites that need to replicate the guard inline (Pages
|
|
52
|
-
* Router worker codegen, Node production server) and for defense-in-depth
|
|
53
|
-
* checks inside redirect emitters.
|
|
54
|
-
*
|
|
55
|
-
* A pathname is considered "open redirect shaped" when its first segment,
|
|
56
|
-
* after decoding backslashes and encoded delimiters, would cause a browser
|
|
57
|
-
* to resolve a `Location` containing the pathname as protocol-relative:
|
|
58
|
-
*
|
|
59
|
-
* - literal `//evil.com`
|
|
60
|
-
* - literal `/\evil.com` (browsers normalize `\` to `/`)
|
|
61
|
-
* - encoded `/%5Cevil.com` (`%5C` decodes to `\` in Location)
|
|
62
|
-
* - encoded `/%2F/evil.com` (`%2F` decodes to `/` → `//`)
|
|
63
|
-
* - mixed `/%5C%2F`, `/%5C%5C` (and other combinations)
|
|
64
|
-
*
|
|
65
|
-
* We explicitly do not require a valid percent sequence elsewhere in the
|
|
66
|
-
* pathname — we only examine the leading bytes (up to the second real or
|
|
67
|
-
* encoded delimiter) so malformed suffixes can still reach the normal
|
|
68
|
-
* "400 Bad Request" decode path instead of being masked as "404".
|
|
69
|
-
*/
|
|
70
|
-
function isOpenRedirectShaped(rawPathname) {
|
|
71
|
-
if (!rawPathname.startsWith("/")) return false;
|
|
72
|
-
const afterSlash = rawPathname.slice(1);
|
|
73
|
-
if (afterSlash.startsWith("/") || afterSlash.startsWith("\\")) return true;
|
|
74
|
-
if (afterSlash.length >= 3 && afterSlash[0] === "%") {
|
|
75
|
-
const encoded = afterSlash.slice(0, 3).toLowerCase();
|
|
76
|
-
if (encoded === "%5c" || encoded === "%2f") return true;
|
|
77
|
-
}
|
|
78
|
-
return false;
|
|
79
|
-
}
|
|
80
48
|
const FILE_LIKE_PATHNAME_RE = /\.[^/]+\/?$/;
|
|
81
49
|
function isWellKnownPathname(pathname) {
|
|
82
50
|
return pathname === "/.well-known" || pathname.startsWith("/.well-known/");
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { RenderObservation } from "../server/cache-proof.js";
|
|
2
|
+
|
|
3
|
+
//#region src/shims/cache-handler.d.ts
|
|
4
|
+
type CacheHandlerValue = {
|
|
5
|
+
lastModified: number;
|
|
6
|
+
age?: number;
|
|
7
|
+
cacheState?: string;
|
|
8
|
+
cacheControl?: CacheControlMetadata;
|
|
9
|
+
value: IncrementalCacheValue | null;
|
|
10
|
+
};
|
|
11
|
+
type CacheControlMetadata = {
|
|
12
|
+
revalidate: number;
|
|
13
|
+
expire?: number;
|
|
14
|
+
};
|
|
15
|
+
type IncrementalCacheValue = CachedFetchValue | CachedAppPageValue | CachedPagesValue | CachedRouteValue | CachedRedirectValue | CachedImageValue;
|
|
16
|
+
type CachedFetchValue = {
|
|
17
|
+
kind: "FETCH";
|
|
18
|
+
data: {
|
|
19
|
+
headers: Record<string, string>;
|
|
20
|
+
body: string;
|
|
21
|
+
url: string;
|
|
22
|
+
status?: number;
|
|
23
|
+
};
|
|
24
|
+
tags?: string[];
|
|
25
|
+
revalidate: number | false;
|
|
26
|
+
};
|
|
27
|
+
type CachedAppPageValue = {
|
|
28
|
+
kind: "APP_PAGE";
|
|
29
|
+
html: string;
|
|
30
|
+
rscData: ArrayBuffer | undefined;
|
|
31
|
+
headers: Record<string, string | string[]> | undefined;
|
|
32
|
+
postponed: string | undefined;
|
|
33
|
+
renderObservation?: RenderObservation;
|
|
34
|
+
status: number | undefined;
|
|
35
|
+
};
|
|
36
|
+
type CachedPagesValue = {
|
|
37
|
+
kind: "PAGES";
|
|
38
|
+
html: string;
|
|
39
|
+
pageData: object;
|
|
40
|
+
generatedFromDataRequest?: boolean;
|
|
41
|
+
headers: Record<string, string | string[]> | undefined;
|
|
42
|
+
status: number | undefined;
|
|
43
|
+
};
|
|
44
|
+
type CachedRouteValue = {
|
|
45
|
+
kind: "APP_ROUTE";
|
|
46
|
+
body: ArrayBuffer;
|
|
47
|
+
status: number;
|
|
48
|
+
headers: Record<string, string | string[]>;
|
|
49
|
+
};
|
|
50
|
+
type CachedRedirectValue = {
|
|
51
|
+
kind: "REDIRECT";
|
|
52
|
+
props: object;
|
|
53
|
+
};
|
|
54
|
+
type CachedImageValue = {
|
|
55
|
+
kind: "IMAGE";
|
|
56
|
+
etag: string;
|
|
57
|
+
buffer: ArrayBuffer;
|
|
58
|
+
extension: string;
|
|
59
|
+
revalidate?: number;
|
|
60
|
+
};
|
|
61
|
+
type CacheHandlerContext = {
|
|
62
|
+
dev?: boolean;
|
|
63
|
+
maxMemoryCacheSize?: number;
|
|
64
|
+
revalidatedTags?: string[];
|
|
65
|
+
[key: string]: unknown;
|
|
66
|
+
};
|
|
67
|
+
type CacheHandler = {
|
|
68
|
+
get(key: string, ctx?: Record<string, unknown>): Promise<CacheHandlerValue | null>;
|
|
69
|
+
set(key: string, data: IncrementalCacheValue | null, ctx?: Record<string, unknown>): Promise<void>;
|
|
70
|
+
revalidateTag(tags: string | string[], durations?: {
|
|
71
|
+
expire?: number;
|
|
72
|
+
}): Promise<void>;
|
|
73
|
+
resetRequestCache?(): void;
|
|
74
|
+
};
|
|
75
|
+
declare class NoOpCacheHandler implements CacheHandler {
|
|
76
|
+
get(_key: string, _ctx?: Record<string, unknown>): Promise<CacheHandlerValue | null>;
|
|
77
|
+
set(_key: string, _data: IncrementalCacheValue | null, _ctx?: Record<string, unknown>): Promise<void>;
|
|
78
|
+
revalidateTag(_tags: string | string[], _durations?: {
|
|
79
|
+
expire?: number;
|
|
80
|
+
}): Promise<void>;
|
|
81
|
+
}
|
|
82
|
+
type MemoryCacheHandlerOptions = Pick<CacheHandlerContext, "maxMemoryCacheSize"> & {
|
|
83
|
+
cacheMaxMemorySize?: number;
|
|
84
|
+
};
|
|
85
|
+
declare class MemoryCacheHandler implements CacheHandler {
|
|
86
|
+
private store;
|
|
87
|
+
private tagRevalidatedAt;
|
|
88
|
+
private readonly maxMemoryCacheSize;
|
|
89
|
+
private currentMemoryCacheSize;
|
|
90
|
+
constructor(options?: number | MemoryCacheHandlerOptions);
|
|
91
|
+
private estimateEntrySize;
|
|
92
|
+
private deleteEntry;
|
|
93
|
+
private touchEntry;
|
|
94
|
+
private evictLeastRecentlyUsed;
|
|
95
|
+
get(key: string, ctx?: Record<string, unknown>): Promise<CacheHandlerValue | null>;
|
|
96
|
+
set(key: string, data: IncrementalCacheValue | null, ctx?: Record<string, unknown>): Promise<void>;
|
|
97
|
+
revalidateTag(tags: string | string[]): Promise<void>;
|
|
98
|
+
resetRequestCache(): void;
|
|
99
|
+
}
|
|
100
|
+
declare function configureMemoryCacheHandler(options?: MemoryCacheHandlerOptions): void;
|
|
101
|
+
declare function setDataCacheHandler(handler: CacheHandler): void;
|
|
102
|
+
declare function getDataCacheHandler(): CacheHandler;
|
|
103
|
+
declare function setCacheHandler(handler: CacheHandler): void;
|
|
104
|
+
declare function getCacheHandler(): CacheHandler;
|
|
105
|
+
//#endregion
|
|
106
|
+
export { CacheControlMetadata, CacheHandler, CacheHandlerContext, CacheHandlerValue, CachedAppPageValue, CachedFetchValue, CachedImageValue, CachedPagesValue, CachedRedirectValue, CachedRouteValue, IncrementalCacheValue, MemoryCacheHandler, NoOpCacheHandler, configureMemoryCacheHandler, getCacheHandler, getDataCacheHandler, setCacheHandler, setDataCacheHandler };
|