Skip to content

Commit 2fb6ca3

Browse files
authored
Merge branch 'canary' into fix-revalidatetag-rsc
2 parents 02cd740 + 7fddec2 commit 2fb6ca3

File tree

16 files changed

+342
-8
lines changed

16 files changed

+342
-8
lines changed

packages/next/server.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,5 +14,6 @@ export { URLPattern } from 'next/dist/compiled/@edge-runtime/primitives/url'
1414
export { ImageResponse } from 'next/dist/server/web/spec-extension/image-response'
1515
export type { ImageResponseOptions } from 'next/dist/compiled/@vercel/og/types'
1616
export { unstable_after } from 'next/dist/server/after'
17+
export { connection } from 'next/dist/server/request/connection'
1718
export type { UnsafeUnwrappedSearchParams } from 'next/dist/server/request/search-params'
1819
export type { UnsafeUnwrappedParams } from 'next/dist/server/request/params'

packages/next/server.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ const serverExports = {
1212
URLPattern: require('next/dist/server/web/spec-extension/url-pattern')
1313
.URLPattern,
1414
unstable_after: require('next/dist/server/after').unstable_after,
15+
connection: require('next/dist/server/request/connection').connection,
1516
}
1617

1718
// https://nodejs.org/api/esm.html#commonjs-namespaces
@@ -26,3 +27,4 @@ exports.userAgentFromString = serverExports.userAgentFromString
2627
exports.userAgent = serverExports.userAgent
2728
exports.URLPattern = serverExports.URLPattern
2829
exports.unstable_after = serverExports.unstable_after
30+
exports.connection = serverExports.connection

packages/next/src/server/app-render/action-handler.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -725,6 +725,7 @@ export async function handleAction({
725725
if (isMultipartAction) {
726726
if (isFetchAction) {
727727
const busboy = (require('busboy') as typeof import('busboy'))({
728+
defParamCharset: 'utf8',
728729
headers: req.headers,
729730
limits: { fieldSize: bodySizeLimitBytes },
730731
})
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import { staticGenerationAsyncStorage } from '../../client/components/static-generation-async-storage.external'
2+
import {
3+
isDynamicIOPrerender,
4+
prerenderAsyncStorage,
5+
} from '../app-render/prerender-async-storage.external'
6+
import {
7+
postponeWithTracking,
8+
throwToInterruptStaticGeneration,
9+
trackDynamicDataInDynamicRender,
10+
} from '../app-render/dynamic-rendering'
11+
import { StaticGenBailoutError } from '../../client/components/static-generation-bailout'
12+
import { makeHangingPromise } from '../dynamic-rendering-utils'
13+
14+
/**
15+
* This function allows you to indicate that you require an actual user Request before continuing.
16+
*
17+
* During prerendering it will never resolve and during rendering it resolves immediately.
18+
*/
19+
export function connection(): Promise<void> {
20+
const staticGenerationStore = staticGenerationAsyncStorage.getStore()
21+
const prerenderStore = prerenderAsyncStorage.getStore()
22+
23+
if (staticGenerationStore) {
24+
if (staticGenerationStore.forceStatic) {
25+
// When using forceStatic we override all other logic and always just return an empty
26+
// headers object without tracking
27+
return Promise.resolve(undefined)
28+
}
29+
30+
if (staticGenerationStore.isUnstableCacheCallback) {
31+
throw new Error(
32+
`Route ${staticGenerationStore.route} used "connection" inside a function cached with "unstable_cache(...)". The \`connection()\` function is used to indicate the subsequent code must only run when there is an actual Request, but caches must be able to be produced before a Request so this function is not allowed in this scope. See more info here: https://nextjs.org/docs/app/api-reference/functions/unstable_cache`
33+
)
34+
} else if (staticGenerationStore.dynamicShouldError) {
35+
throw new StaticGenBailoutError(
36+
`Route ${staticGenerationStore.route} with \`dynamic = "error"\` couldn't be rendered statically because it used \`connection\`. See more info here: https://nextjs.org/docs/app/building-your-application/rendering/static-and-dynamic#dynamic-rendering`
37+
)
38+
}
39+
40+
if (prerenderStore) {
41+
// We are in PPR and/or dynamicIO mode and prerendering
42+
43+
if (isDynamicIOPrerender(prerenderStore)) {
44+
// We use the controller and cacheSignal as an indication we are in dynamicIO mode.
45+
// When resolving headers for a prerender with dynamic IO we return a forever promise
46+
// along with property access tracked synchronous headers.
47+
48+
// We don't track dynamic access here because access will be tracked when you access
49+
// one of the properties of the headers object.
50+
return makeHangingPromise()
51+
} else {
52+
// We are prerendering with PPR. We need track dynamic access here eagerly
53+
// to keep continuity with how headers has worked in PPR without dynamicIO.
54+
// TODO consider switching the semantic to throw on property access intead
55+
postponeWithTracking(
56+
staticGenerationStore.route,
57+
'connection',
58+
prerenderStore.dynamicTracking
59+
)
60+
}
61+
} else if (staticGenerationStore.isStaticGeneration) {
62+
// We are in a legacy static generation mode while prerendering
63+
// We treat this function call as a bailout of static generation
64+
throwToInterruptStaticGeneration('connection', staticGenerationStore)
65+
}
66+
// We fall through to the dynamic context below but we still track dynamic access
67+
// because in dev we can still error for things like using headers inside a cache context
68+
trackDynamicDataInDynamicRender(staticGenerationStore)
69+
}
70+
71+
return Promise.resolve(undefined)
72+
}

packages/next/src/server/web/exports/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,4 @@ export { NextResponse } from '../spec-extension/response'
66
export { userAgent, userAgentFromString } from '../spec-extension/user-agent'
77
export { URLPattern } from '../spec-extension/url-pattern'
88
export { unstable_after } from '../../after'
9+
export { connection } from '../../request/connection'

test/e2e/app-dir/actions/app-action.test.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -336,7 +336,7 @@ describe('app-dir action handling', () => {
336336

337337
// Fake a file to upload
338338
await browser.eval(`
339-
const file = new File(['hello'], 'hello.txt', { type: 'text/plain' });
339+
const file = new File(['hello'], 'hello你好テスト.txt', { type: 'text/plain' });
340340
const list = new DataTransfer();
341341
list.items.add(file);
342342
document.getElementById('file').files = list.files;
@@ -347,7 +347,9 @@ describe('app-dir action handling', () => {
347347
// we don't have access to runtime logs on deploy
348348
if (!isNextDeploy) {
349349
await check(() => {
350-
return logs.some((log) => log.includes('File name: hello.txt size: 5'))
350+
return logs.some((log) =>
351+
log.includes('File name: hello你好テスト.txt size: 5')
352+
)
351353
? 'yes'
352354
: ''
353355
}, 'yes')

test/e2e/app-dir/dynamic-data/dynamic-data.test.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,16 @@ describe('dynamic-data with dynamic = "error"', () => {
199199
await browser.close()
200200
}
201201

202+
browser = await next.browser('/connection')
203+
try {
204+
await assertHasRedbox(browser)
205+
expect(await getRedboxHeader(browser)).toMatch(
206+
'Error: Route /connection with `dynamic = "error"` couldn\'t be rendered statically because it used `connection`'
207+
)
208+
} finally {
209+
await browser.close()
210+
}
211+
202212
browser = await next.browser('/headers?foo=foosearch')
203213
try {
204214
await assertHasRedbox(browser)
@@ -230,6 +240,9 @@ describe('dynamic-data with dynamic = "error"', () => {
230240
expect(next.cliOutput).toMatch(
231241
'Error: Route /cookies with `dynamic = "error"` couldn\'t be rendered statically because it used `cookies`'
232242
)
243+
expect(next.cliOutput).toMatch(
244+
'Error: Route /connection with `dynamic = "error"` couldn\'t be rendered statically because it used `connection`'
245+
)
233246
expect(next.cliOutput).toMatch(
234247
'Error: Route /headers with `dynamic = "error"` couldn\'t be rendered statically because it used `headers`'
235248
)
@@ -277,6 +290,16 @@ describe('dynamic-data inside cache scope', () => {
277290
await browser.close()
278291
}
279292

293+
browser = await next.browser('/connection')
294+
try {
295+
await assertHasRedbox(browser)
296+
expect(await getRedboxHeader(browser)).toMatch(
297+
'Error: Route /connection used "connection" inside a function cached with "unstable_cache(...)".'
298+
)
299+
} finally {
300+
await browser.close()
301+
}
302+
280303
browser = await next.browser('/headers')
281304
try {
282305
await assertHasRedbox(browser)
@@ -297,6 +320,9 @@ describe('dynamic-data inside cache scope', () => {
297320
expect(next.cliOutput).toMatch(
298321
'Error: Route /cookies used "cookies" inside a function cached with "unstable_cache(...)".'
299322
)
323+
expect(next.cliOutput).toMatch(
324+
'Error: Route /connection used "connection" inside a function cached with "unstable_cache(...)".'
325+
)
300326
expect(next.cliOutput).toMatch(
301327
'Error: Route /headers used "headers" inside a function cached with "unstable_cache(...)".'
302328
)
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import { connection } from 'next/server'
2+
import { unstable_cache as cache } from 'next/cache'
3+
4+
const cachedConnection = cache(async () => connection())
5+
6+
export default async function Page({ searchParams }) {
7+
await cachedConnection()
8+
return (
9+
<div>
10+
<section>
11+
This example uses `connection()` inside `unstable_cache` which should
12+
cause the build to fail
13+
</section>
14+
</div>
15+
)
16+
}

test/e2e/app-dir/dynamic-data/fixtures/main/app/force-dynamic/page.js

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,19 @@
11
import { headers, cookies } from 'next/headers'
2+
import { connection } from 'next/server'
23

34
import { PageSentinel } from '../getSentinelValue'
45

56
export const dynamic = 'force-dynamic'
67

78
export default async function Page({ searchParams }) {
9+
await connection()
810
return (
911
<div>
1012
<PageSentinel />
1113
<section>
12-
This example uses headers/cookies/searchParams directly in a Page
13-
configured with `dynamic = 'force-dynamic'`. This should cause the page
14-
to always render dynamically regardless of dynamic APIs used
14+
This example uses headers/cookies/connection/searchParams directly in a
15+
Page configured with `dynamic = 'force-dynamic'`. This should cause the
16+
page to always render dynamically regardless of dynamic APIs used
1517
</section>
1618
<section id="headers">
1719
<h3>headers</h3>

test/e2e/app-dir/dynamic-data/fixtures/main/app/force-static/page.js

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,19 @@
11
import { headers, cookies } from 'next/headers'
2+
import { connection } from 'next/server'
23

34
import { PageSentinel } from '../getSentinelValue'
45

56
export const dynamic = 'force-static'
67

78
export default async function Page({ searchParams }) {
9+
await connection()
810
return (
911
<div>
1012
<PageSentinel />
1113
<section>
12-
This example uses headers/cookies/searchParams directly in a Page
13-
configured with `dynamic = 'force-static'`. This should cause the page
14-
to always statically render but without exposing dynamic data
14+
This example uses headers/cookies/connection/searchParams directly in a
15+
Page configured with `dynamic = 'force-static'`. This should cause the
16+
page to always statically render but without exposing dynamic data
1517
</section>
1618
<section id="headers">
1719
<h3>headers</h3>

0 commit comments

Comments
 (0)