securityos/node_modules/next/dist/esm/server/lib/patch-fetch.js

461 lines
22 KiB
JavaScript

import { AppRenderSpan, NextNodeServerSpan } from "./trace/constants";
import { getTracer, SpanKind } from "./trace/tracer";
import { CACHE_ONE_YEAR, NEXT_CACHE_IMPLICIT_TAG_ID, NEXT_CACHE_TAG_MAX_LENGTH } from "../../lib/constants";
import * as Log from "../../build/output/log";
const isEdgeRuntime = process.env.NEXT_RUNTIME === "edge";
export function validateTags(tags, description) {
const validTags = [];
const invalidTags = [];
for (const tag of tags){
if (typeof tag !== "string") {
invalidTags.push({
tag,
reason: "invalid type, must be a string"
});
} else if (tag.length > NEXT_CACHE_TAG_MAX_LENGTH) {
invalidTags.push({
tag,
reason: `exceeded max length of ${NEXT_CACHE_TAG_MAX_LENGTH}`
});
} else {
validTags.push(tag);
}
}
if (invalidTags.length > 0) {
console.warn(`Warning: invalid tags passed to ${description}: `);
for (const { tag, reason } of invalidTags){
console.log(`tag: "${tag}" ${reason}`);
}
}
return validTags;
}
const getDerivedTags = (pathname)=>{
const derivedTags = [
`/layout`
];
// we automatically add the current path segments as tags
// for revalidatePath handling
if (pathname.startsWith("/")) {
const pathnameParts = pathname.split("/");
for(let i = 1; i < pathnameParts.length + 1; i++){
let curPathname = pathnameParts.slice(0, i).join("/");
if (curPathname) {
// all derived tags other than the page are layout tags
if (!curPathname.endsWith("/page") && !curPathname.endsWith("/route")) {
curPathname = `${curPathname}${!curPathname.endsWith("/") ? "/" : ""}layout`;
}
derivedTags.push(curPathname);
}
}
}
return derivedTags;
};
export function addImplicitTags(staticGenerationStore) {
const newTags = [];
if (!staticGenerationStore) {
return newTags;
}
const { pagePath, urlPathname } = staticGenerationStore;
if (!Array.isArray(staticGenerationStore.tags)) {
staticGenerationStore.tags = [];
}
if (pagePath) {
const derivedTags = getDerivedTags(pagePath);
for (let tag of derivedTags){
var _staticGenerationStore_tags;
tag = `${NEXT_CACHE_IMPLICIT_TAG_ID}${tag}`;
if (!((_staticGenerationStore_tags = staticGenerationStore.tags) == null ? void 0 : _staticGenerationStore_tags.includes(tag))) {
staticGenerationStore.tags.push(tag);
}
newTags.push(tag);
}
}
if (urlPathname) {
var _staticGenerationStore_tags1;
const tag = `${NEXT_CACHE_IMPLICIT_TAG_ID}${urlPathname}`;
if (!((_staticGenerationStore_tags1 = staticGenerationStore.tags) == null ? void 0 : _staticGenerationStore_tags1.includes(tag))) {
staticGenerationStore.tags.push(tag);
}
newTags.push(tag);
}
return newTags;
}
function trackFetchMetric(staticGenerationStore, ctx) {
if (!staticGenerationStore) return;
if (!staticGenerationStore.fetchMetrics) {
staticGenerationStore.fetchMetrics = [];
}
const dedupeFields = [
"url",
"status",
"method"
];
// don't add metric if one already exists for the fetch
if (staticGenerationStore.fetchMetrics.some((metric)=>{
return dedupeFields.every((field)=>metric[field] === ctx[field]);
})) {
return;
}
staticGenerationStore.fetchMetrics.push({
url: ctx.url,
cacheStatus: ctx.cacheStatus,
cacheReason: ctx.cacheReason,
status: ctx.status,
method: ctx.method,
start: ctx.start,
end: Date.now(),
idx: staticGenerationStore.nextFetchId || 0
});
}
// we patch fetch to collect cache information used for
// determining if a page is static or not
export function patchFetch({ serverHooks, staticGenerationAsyncStorage }) {
if (!globalThis._nextOriginalFetch) {
globalThis._nextOriginalFetch = globalThis.fetch;
}
if (globalThis.fetch.__nextPatched) return;
const { DynamicServerError } = serverHooks;
const originFetch = globalThis._nextOriginalFetch;
globalThis.fetch = async (input, init)=>{
var _init_method, _this;
let url;
try {
url = new URL(input instanceof Request ? input.url : input);
url.username = "";
url.password = "";
} catch {
// Error caused by malformed URL should be handled by native fetch
url = undefined;
}
const fetchUrl = (url == null ? void 0 : url.href) ?? "";
const fetchStart = Date.now();
const method = (init == null ? void 0 : (_init_method = init.method) == null ? void 0 : _init_method.toUpperCase()) || "GET";
// Do create a new span trace for internal fetches in the
// non-verbose mode.
const isInternal = ((_this = init == null ? void 0 : init.next) == null ? void 0 : _this.internal) === true;
return await getTracer().trace(isInternal ? NextNodeServerSpan.internalFetch : AppRenderSpan.fetch, {
kind: SpanKind.CLIENT,
spanName: [
"fetch",
method,
fetchUrl
].filter(Boolean).join(" "),
attributes: {
"http.url": fetchUrl,
"http.method": method,
"net.peer.name": url == null ? void 0 : url.hostname,
"net.peer.port": (url == null ? void 0 : url.port) || undefined
}
}, async ()=>{
var _getRequestMeta;
const staticGenerationStore = staticGenerationAsyncStorage.getStore() || (fetch.__nextGetStaticStore == null ? void 0 : fetch.__nextGetStaticStore.call(fetch));
const isRequestInput = input && typeof input === "object" && typeof input.method === "string";
const getRequestMeta = (field)=>{
let value = isRequestInput ? input[field] : null;
return value || (init == null ? void 0 : init[field]);
};
// If the staticGenerationStore is not available, we can't do any
// special treatment of fetch, therefore fallback to the original
// fetch implementation.
if (!staticGenerationStore || isInternal || staticGenerationStore.isDraftMode) {
return originFetch(input, init);
}
let revalidate = undefined;
const getNextField = (field)=>{
var _init_next, _init_next1, _input_next;
return typeof (init == null ? void 0 : (_init_next = init.next) == null ? void 0 : _init_next[field]) !== "undefined" ? init == null ? void 0 : (_init_next1 = init.next) == null ? void 0 : _init_next1[field] : isRequestInput ? (_input_next = input.next) == null ? void 0 : _input_next[field] : undefined;
};
// RequestInit doesn't keep extra fields e.g. next so it's
// only available if init is used separate
let curRevalidate = getNextField("revalidate");
const tags = validateTags(getNextField("tags") || [], `fetch ${input.toString()}`);
if (Array.isArray(tags)) {
if (!staticGenerationStore.tags) {
staticGenerationStore.tags = [];
}
for (const tag of tags){
if (!staticGenerationStore.tags.includes(tag)) {
staticGenerationStore.tags.push(tag);
}
}
}
const implicitTags = addImplicitTags(staticGenerationStore);
const isOnlyCache = staticGenerationStore.fetchCache === "only-cache";
const isForceCache = staticGenerationStore.fetchCache === "force-cache";
const isDefaultCache = staticGenerationStore.fetchCache === "default-cache";
const isDefaultNoStore = staticGenerationStore.fetchCache === "default-no-store";
const isOnlyNoStore = staticGenerationStore.fetchCache === "only-no-store";
const isForceNoStore = staticGenerationStore.fetchCache === "force-no-store";
let _cache = getRequestMeta("cache");
let cacheReason = "";
if (typeof _cache === "string" && typeof curRevalidate !== "undefined") {
Log.warn(`fetch for ${fetchUrl} on ${staticGenerationStore.urlPathname} specified "cache: ${_cache}" and "revalidate: ${curRevalidate}", only one should be specified.`);
_cache = undefined;
}
if (_cache === "force-cache") {
curRevalidate = false;
}
if ([
"no-cache",
"no-store"
].includes(_cache || "")) {
curRevalidate = 0;
cacheReason = `cache: ${_cache}`;
}
if (typeof curRevalidate === "number" || curRevalidate === false) {
revalidate = curRevalidate;
}
const _headers = getRequestMeta("headers");
const initHeaders = typeof (_headers == null ? void 0 : _headers.get) === "function" ? _headers : new Headers(_headers || {});
const hasUnCacheableHeader = initHeaders.get("authorization") || initHeaders.get("cookie");
const isUnCacheableMethod = ![
"get",
"head"
].includes(((_getRequestMeta = getRequestMeta("method")) == null ? void 0 : _getRequestMeta.toLowerCase()) || "get");
// if there are authorized headers or a POST method and
// dynamic data usage was present above the tree we bail
// e.g. if cookies() is used before an authed/POST fetch
const autoNoCache = (hasUnCacheableHeader || isUnCacheableMethod) && staticGenerationStore.revalidate === 0;
if (isForceNoStore) {
revalidate = 0;
cacheReason = "fetchCache = force-no-store";
}
if (isOnlyNoStore) {
if (_cache === "force-cache" || revalidate === 0) {
throw new Error(`cache: 'force-cache' used on fetch for ${fetchUrl} with 'export const fetchCache = 'only-no-store'`);
}
revalidate = 0;
cacheReason = "fetchCache = only-no-store";
}
if (isOnlyCache && _cache === "no-store") {
throw new Error(`cache: 'no-store' used on fetch for ${fetchUrl} with 'export const fetchCache = 'only-cache'`);
}
if (isForceCache && (typeof curRevalidate === "undefined" || curRevalidate === 0)) {
cacheReason = "fetchCache = force-cache";
revalidate = false;
}
if (typeof revalidate === "undefined") {
if (isDefaultCache) {
revalidate = false;
cacheReason = "fetchCache = default-cache";
} else if (autoNoCache) {
revalidate = 0;
cacheReason = "auto no cache";
} else if (isDefaultNoStore) {
revalidate = 0;
cacheReason = "fetchCache = default-no-store";
} else {
cacheReason = "auto cache";
revalidate = typeof staticGenerationStore.revalidate === "boolean" || typeof staticGenerationStore.revalidate === "undefined" ? false : staticGenerationStore.revalidate;
}
} else if (!cacheReason) {
cacheReason = `revalidate: ${revalidate}`;
}
if (// we don't consider autoNoCache to switch to dynamic during
// revalidate although if it occurs during build we do
!autoNoCache && (typeof staticGenerationStore.revalidate === "undefined" || typeof revalidate === "number" && (staticGenerationStore.revalidate === false || typeof staticGenerationStore.revalidate === "number" && revalidate < staticGenerationStore.revalidate))) {
staticGenerationStore.revalidate = revalidate;
}
const isCacheableRevalidate = typeof revalidate === "number" && revalidate > 0 || revalidate === false;
let cacheKey;
if (staticGenerationStore.incrementalCache && isCacheableRevalidate) {
try {
cacheKey = await staticGenerationStore.incrementalCache.fetchCacheKey(fetchUrl, isRequestInput ? input : init);
} catch (err) {
console.error(`Failed to generate cache key for`, input);
}
}
const fetchIdx = staticGenerationStore.nextFetchId ?? 1;
staticGenerationStore.nextFetchId = fetchIdx + 1;
const normalizedRevalidate = typeof revalidate !== "number" ? CACHE_ONE_YEAR : revalidate;
const doOriginalFetch = async (isStale, cacheReasonOverride)=>{
const requestInputFields = [
"cache",
"credentials",
"headers",
"integrity",
"keepalive",
"method",
"mode",
"redirect",
"referrer",
"referrerPolicy",
"window",
"duplex",
// don't pass through signal when revalidating
...isStale ? [] : [
"signal"
]
];
if (isRequestInput) {
const reqInput = input;
const reqOptions = {
body: reqInput._ogBody || reqInput.body
};
for (const field of requestInputFields){
// @ts-expect-error custom fields
reqOptions[field] = reqInput[field];
}
input = new Request(reqInput.url, reqOptions);
} else if (init) {
const initialInit = init;
init = {
body: init._ogBody || init.body
};
for (const field of requestInputFields){
// @ts-expect-error custom fields
init[field] = initialInit[field];
}
}
// add metadata to init without editing the original
const clonedInit = {
...init,
next: {
...init == null ? void 0 : init.next,
fetchType: "origin",
fetchIdx
}
};
return originFetch(input, clonedInit).then(async (res)=>{
if (!isStale) {
trackFetchMetric(staticGenerationStore, {
start: fetchStart,
url: fetchUrl,
cacheReason: cacheReasonOverride || cacheReason,
cacheStatus: revalidate === 0 || cacheReasonOverride ? "skip" : "miss",
status: res.status,
method: clonedInit.method || "GET"
});
}
if (res.status === 200 && staticGenerationStore.incrementalCache && cacheKey && isCacheableRevalidate) {
const bodyBuffer = Buffer.from(await res.arrayBuffer());
try {
await staticGenerationStore.incrementalCache.set(cacheKey, {
kind: "FETCH",
data: {
headers: Object.fromEntries(res.headers.entries()),
body: bodyBuffer.toString("base64"),
status: res.status,
url: res.url
},
revalidate: normalizedRevalidate
}, {
fetchCache: true,
revalidate,
fetchUrl,
fetchIdx,
tags
});
} catch (err) {
console.warn(`Failed to set fetch cache`, input, err);
}
const response = new Response(bodyBuffer, {
headers: new Headers(res.headers),
status: res.status
});
Object.defineProperty(response, "url", {
value: res.url
});
return response;
}
return res;
});
};
let handleUnlock = ()=>Promise.resolve();
let cacheReasonOverride;
if (cacheKey && staticGenerationStore.incrementalCache) {
handleUnlock = await staticGenerationStore.incrementalCache.lock(cacheKey);
const entry = staticGenerationStore.isOnDemandRevalidate ? null : await staticGenerationStore.incrementalCache.get(cacheKey, {
fetchCache: true,
revalidate,
fetchUrl,
fetchIdx,
tags,
softTags: implicitTags
});
if (entry) {
await handleUnlock();
} else {
// in dev, incremental cache response will be null in case the browser adds `cache-control: no-cache` in the request headers
cacheReasonOverride = "cache-control: no-cache (hard refresh)";
}
if ((entry == null ? void 0 : entry.value) && entry.value.kind === "FETCH") {
// when stale and is revalidating we wait for fresh data
// so the revalidated entry has the updated data
if (!(staticGenerationStore.isRevalidate && entry.isStale)) {
if (entry.isStale) {
if (!staticGenerationStore.pendingRevalidates) {
staticGenerationStore.pendingRevalidates = [];
}
staticGenerationStore.pendingRevalidates.push(doOriginalFetch(true).catch(console.error));
}
const resData = entry.value.data;
let decodedBody;
if (process.env.NEXT_RUNTIME === "edge") {
const { decode } = require("../../shared/lib/base64-arraybuffer");
decodedBody = decode(resData.body);
} else {
decodedBody = Buffer.from(resData.body, "base64").subarray();
}
trackFetchMetric(staticGenerationStore, {
start: fetchStart,
url: fetchUrl,
cacheReason,
cacheStatus: "hit",
status: resData.status || 200,
method: (init == null ? void 0 : init.method) || "GET"
});
const response = new Response(decodedBody, {
headers: resData.headers,
status: resData.status
});
Object.defineProperty(response, "url", {
value: entry.value.data.url
});
return response;
}
}
}
if (staticGenerationStore.isStaticGeneration) {
if (init && typeof init === "object") {
const cache = init.cache;
// Delete `cache` property as Cloudflare Workers will throw an error
if (isEdgeRuntime) {
delete init.cache;
}
if (cache === "no-store") {
staticGenerationStore.revalidate = 0;
const dynamicUsageReason = `no-store fetch ${input}${staticGenerationStore.urlPathname ? ` ${staticGenerationStore.urlPathname}` : ""}`;
const err = new DynamicServerError(dynamicUsageReason);
staticGenerationStore.dynamicUsageErr = err;
staticGenerationStore.dynamicUsageStack = err.stack;
staticGenerationStore.dynamicUsageDescription = dynamicUsageReason;
}
const hasNextConfig = "next" in init;
const next = init.next || {};
if (typeof next.revalidate === "number" && (typeof staticGenerationStore.revalidate === "undefined" || typeof staticGenerationStore.revalidate === "number" && next.revalidate < staticGenerationStore.revalidate)) {
const forceDynamic = staticGenerationStore.forceDynamic;
if (!forceDynamic || next.revalidate !== 0) {
staticGenerationStore.revalidate = next.revalidate;
}
if (!forceDynamic && next.revalidate === 0) {
const dynamicUsageReason = `revalidate: ${next.revalidate} fetch ${input}${staticGenerationStore.urlPathname ? ` ${staticGenerationStore.urlPathname}` : ""}`;
const err = new DynamicServerError(dynamicUsageReason);
staticGenerationStore.dynamicUsageErr = err;
staticGenerationStore.dynamicUsageStack = err.stack;
staticGenerationStore.dynamicUsageDescription = dynamicUsageReason;
}
}
if (hasNextConfig) delete init.next;
}
}
return doOriginalFetch(false, cacheReasonOverride).finally(handleUnlock);
});
};
globalThis.fetch.__nextGetStaticStore = ()=>{
return staticGenerationAsyncStorage;
};
globalThis.fetch.__nextPatched = true;
}
//# sourceMappingURL=patch-fetch.js.map