diff --git a/src/run/handlers/cache.cts b/src/run/handlers/cache.cts index 4e9b61ce4a..37ade76962 100644 --- a/src/run/handlers/cache.cts +++ b/src/run/handlers/cache.cts @@ -332,6 +332,11 @@ export class NetlifyCacheHandler implements CacheHandlerForMultipleVersions { } } case 'APP_PAGE': { + const requestContext = getRequestContext() + if (requestContext && blob.value?.kind === 'APP_PAGE') { + requestContext.isCacheableAppPage = true + } + const { revalidate, rscData, ...restOfPageValue } = blob.value span.addEvent(blob.value?.kind, { lastModified: blob.lastModified, revalidate, ttl }) @@ -410,6 +415,13 @@ export class NetlifyCacheHandler implements CacheHandlerForMultipleVersions { await this.cacheStore.set(key, { lastModified, value }, 'blobStore.set') + if (data?.kind === 'APP_PAGE') { + const requestContext = getRequestContext() + if (requestContext) { + requestContext.isCacheableAppPage = true + } + } + if ((!data && !isDataReq) || data?.kind === 'PAGE' || data?.kind === 'PAGES') { const requestContext = getRequestContext() if (requestContext?.didPagesRouterOnDemandRevalidate) { diff --git a/src/run/handlers/request-context.cts b/src/run/handlers/request-context.cts index b351984729..9aabb7ca0a 100644 --- a/src/run/handlers/request-context.cts +++ b/src/run/handlers/request-context.cts @@ -35,6 +35,7 @@ export type RequestContext = { backgroundWorkPromise: Promise logger: SystemLogger requestID: string + isCacheableAppPage?: boolean } type RequestContextAsyncLocalStorage = AsyncLocalStorage diff --git a/src/run/handlers/server.ts b/src/run/handlers/server.ts index 62c7c8f8b8..975f6993fb 100644 --- a/src/run/handlers/server.ts +++ b/src/run/handlers/server.ts @@ -17,7 +17,7 @@ import { nextResponseProxy } from '../revalidate.js' import { setFetchBeforeNextPatchedIt } from '../storage/storage.cjs' import { getLogger, type RequestContext } from './request-context.cjs' -import { getTracer } from './tracer.cjs' +import { getTracer, recordWarning } from './tracer.cjs' import { setupWaitUntil } from './wait-until.cjs' setFetchBeforeNextPatchedIt(globalThis.fetch) @@ -117,11 +117,6 @@ export default async ( const nextCache = response.headers.get('x-nextjs-cache') const isServedFromNextCache = nextCache === 'HIT' || nextCache === 'STALE' - topLevelSpan.setAttributes({ - 'x-nextjs-cache': nextCache ?? undefined, - isServedFromNextCache, - }) - if (isServedFromNextCache) { await adjustDateHeader({ headers: response.headers, @@ -136,6 +131,41 @@ export default async ( setVaryHeaders(response.headers, request, nextConfig) setCacheStatusHeader(response.headers, nextCache) + const netlifyVary = response.headers.get('netlify-vary') ?? undefined + const netlifyCdnCacheControl = response.headers.get('netlify-cdn-cache-control') ?? undefined + topLevelSpan.setAttributes({ + 'x-nextjs-cache': nextCache ?? undefined, + isServedFromNextCache, + netlifyVary, + netlifyCdnCacheControl, + }) + + if (requestContext.isCacheableAppPage) { + const isRSCRequest = request.headers.get('rsc') === '1' + const contentType = response.headers.get('content-type') ?? undefined + + const isExpectedContentType = + ((isRSCRequest && contentType?.includes('text/x-component')) || + (!isRSCRequest && contentType?.includes('text/html'))) ?? + false + + topLevelSpan.setAttributes({ + isRSCRequest, + isCacheableAppPage: true, + contentType, + isExpectedContentType, + }) + + if (!isExpectedContentType) { + recordWarning( + new Error( + `Unexpected content type was produced for App Router page response (isRSCRequest: ${isRSCRequest}, contentType: ${contentType})`, + ), + topLevelSpan, + ) + } + } + async function waitForBackgroundWork() { // it's important to keep the stream open until the next handler has finished await nextHandlerPromise