Skip to content

Commit 530d2c5

Browse files
authored
fix: handle case of zero-length cacheable route handler responses (#2819)
* test: add test case for route with 0-length response * test: add new blob key to test checking prerendered blobs after changing fixture * fix: ensure size calculation returns positive numbers and make in-memory cache failures not fatal * fix: drop unneded extra variable and also capture calculated size in warning
1 parent 8a63ac2 commit 530d2c5

File tree

3 files changed

+80
-24
lines changed

3 files changed

+80
-24
lines changed

src/run/storage/request-scoped-in-memory-cache.cts

+64-24
Original file line numberDiff line numberDiff line change
@@ -25,44 +25,70 @@ export function setInMemoryCacheMaxSizeFromNextConfig(size: unknown) {
2525
}
2626
}
2727

28-
const estimateBlobSize = (valueToStore: BlobType | null | Promise<unknown>): number => {
28+
type PositiveNumber = number & { __positive: true }
29+
const isPositiveNumber = (value: unknown): value is PositiveNumber => {
30+
return typeof value === 'number' && value > 0
31+
}
32+
33+
const BASE_BLOB_SIZE = 25 as PositiveNumber
34+
35+
const estimateBlobKnownTypeSize = (
36+
valueToStore: BlobType | null | Promise<unknown>,
37+
): number | undefined => {
2938
// very approximate size calculation to avoid expensive exact size calculation
3039
// inspired by https://github.com/vercel/next.js/blob/ed10f7ed0246fcc763194197eb9beebcbd063162/packages/next/src/server/lib/incremental-cache/file-system-cache.ts#L60-L79
3140
if (valueToStore === null || isPromise(valueToStore) || isTagManifest(valueToStore)) {
32-
return 25
41+
return BASE_BLOB_SIZE
3342
}
3443
if (isHtmlBlob(valueToStore)) {
35-
return valueToStore.html.length
44+
return BASE_BLOB_SIZE + valueToStore.html.length
45+
}
46+
47+
if (valueToStore.value?.kind === 'FETCH') {
48+
return BASE_BLOB_SIZE + valueToStore.value.data.body.length
49+
}
50+
if (valueToStore.value?.kind === 'APP_PAGE') {
51+
return (
52+
BASE_BLOB_SIZE + valueToStore.value.html.length + (valueToStore.value.rscData?.length ?? 0)
53+
)
54+
}
55+
if (valueToStore.value?.kind === 'PAGE' || valueToStore.value?.kind === 'PAGES') {
56+
return (
57+
BASE_BLOB_SIZE +
58+
valueToStore.value.html.length +
59+
JSON.stringify(valueToStore.value.pageData).length
60+
)
3661
}
37-
let knownKindFailed = false
62+
if (valueToStore.value?.kind === 'ROUTE' || valueToStore.value?.kind === 'APP_ROUTE') {
63+
return BASE_BLOB_SIZE + valueToStore.value.body.length
64+
}
65+
}
66+
67+
const estimateBlobSize = (valueToStore: BlobType | null | Promise<unknown>): PositiveNumber => {
68+
let estimatedKnownTypeSize: number | undefined
69+
let estimateBlobKnownTypeSizeError: unknown
3870
try {
39-
if (valueToStore.value?.kind === 'FETCH') {
40-
return valueToStore.value.data.body.length
41-
}
42-
if (valueToStore.value?.kind === 'APP_PAGE') {
43-
return valueToStore.value.html.length + (valueToStore.value.rscData?.length ?? 0)
71+
estimatedKnownTypeSize = estimateBlobKnownTypeSize(valueToStore)
72+
if (isPositiveNumber(estimatedKnownTypeSize)) {
73+
return estimatedKnownTypeSize
4474
}
45-
if (valueToStore.value?.kind === 'PAGE' || valueToStore.value?.kind === 'PAGES') {
46-
return valueToStore.value.html.length + JSON.stringify(valueToStore.value.pageData).length
47-
}
48-
if (valueToStore.value?.kind === 'ROUTE' || valueToStore.value?.kind === 'APP_ROUTE') {
49-
return valueToStore.value.body.length
50-
}
51-
} catch {
52-
// size calculation rely on the shape of the value, so if it's not what we expect, we fallback to JSON.stringify
53-
knownKindFailed = true
75+
} catch (error) {
76+
estimateBlobKnownTypeSizeError = error
5477
}
5578

56-
// fallback for not known kinds or known kinds that did fail to calculate size
79+
// fallback for not known kinds or known kinds that did fail to calculate positive size
80+
const calculatedSize = JSON.stringify(valueToStore).length
81+
5782
// we should also monitor cases when fallback is used because it's not the most efficient way to calculate/estimate size
5883
// and might indicate need to make adjustments or additions to the size calculation
5984
recordWarning(
6085
new Error(
61-
`Blob size calculation did fallback to JSON.stringify. Kind: KnownKindFailed: ${knownKindFailed}, ${valueToStore.value?.kind ?? 'undefined'}`,
86+
`Blob size calculation did fallback to JSON.stringify. EstimatedKnownTypeSize: ${estimatedKnownTypeSize}, CalculatedSize: ${calculatedSize}, ValueToStore: ${JSON.stringify(valueToStore)}`,
87+
estimateBlobKnownTypeSizeError ? { cause: estimateBlobKnownTypeSizeError } : undefined,
6288
),
6389
)
6490

65-
return JSON.stringify(valueToStore).length
91+
return isPositiveNumber(calculatedSize) ? calculatedSize : BASE_BLOB_SIZE
6692
}
6793

6894
function getInMemoryLRUCache() {
@@ -98,12 +124,26 @@ export const getRequestScopedInMemoryCache = (): RequestScopedInMemoryCache => {
98124
return {
99125
get(key) {
100126
if (!requestContext) return
101-
const value = inMemoryLRUCache?.get(`${requestContext.requestID}:${key}`)
102-
return value === NullValue ? null : value
127+
try {
128+
const value = inMemoryLRUCache?.get(`${requestContext.requestID}:${key}`)
129+
return value === NullValue ? null : value
130+
} catch (error) {
131+
// using in-memory store is perf optimization not requirement
132+
// trying to use optimization should NOT cause crashes
133+
// so we just record warning and return undefined
134+
recordWarning(new Error('Failed to get value from memory cache', { cause: error }))
135+
}
103136
},
104137
set(key, value) {
105138
if (!requestContext) return
106-
inMemoryLRUCache?.set(`${requestContext?.requestID}:${key}`, value ?? NullValue)
139+
try {
140+
inMemoryLRUCache?.set(`${requestContext?.requestID}:${key}`, value ?? NullValue)
141+
} catch (error) {
142+
// using in-memory store is perf optimization not requirement
143+
// trying to use optimization should NOT cause crashes
144+
// so we just record warning and return undefined
145+
recordWarning(new Error('Failed to store value in memory cache', { cause: error }))
146+
}
107147
},
108148
}
109149
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
export async function GET() {
2+
return new Response('')
3+
}
4+
5+
export const dynamic = 'force-static'

tests/integration/cache-handler.test.ts

+11
Original file line numberDiff line numberDiff line change
@@ -367,6 +367,7 @@ describe('plugin', () => {
367367
'/api/revalidate-handler',
368368
'/api/static/first',
369369
'/api/static/second',
370+
'/api/zero-length-response',
370371
'/index',
371372
'/product/事前レンダリング,test',
372373
'/revalidate-fetch',
@@ -508,4 +509,14 @@ describe('route', () => {
508509

509510
expect(call2.body).toBe('{"params":{"slug":"not-in-generateStaticParams"}}')
510511
})
512+
513+
test<FixtureTestContext>('cacheable route handler response with 0 length response is served correctly', async (ctx) => {
514+
await createFixture('server-components', ctx)
515+
await runPlugin(ctx)
516+
517+
const call = await invokeFunction(ctx, { url: '/api/zero-length-response' })
518+
519+
expect(call.statusCode).toBe(200)
520+
expect(call.body).toBe('')
521+
})
511522
})

0 commit comments

Comments
 (0)