Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -144,13 +144,13 @@ pnpm test-dev-turbo test/development/
Generating tests using `pnpm new-test` is mandatory.

```bash
# Use --args for non-interactive mode
# Format: pnpm new-test --args <appDir> <name> <type>
# Use --args for non-interactive mode (forward args to the script using `--`)
# Format: pnpm new-test -- --args <appDir> <name> <type>
# appDir: true/false (is this for app directory?)
# name: test name (e.g. "my-feature")
# type: e2e | production | development | unit

pnpm new-test --args true my-feature e2e
pnpm new-test -- --args true my-feature e2e
```

**Analyzing test output efficiently:**
Expand Down Expand Up @@ -397,7 +397,7 @@ Core runtime/bundling rules (always apply; skills above expand on these with ver
### Test Gotchas

- **Cache components enables PPR by default**: When `__NEXT_CACHE_COMPONENTS=true`, most app-dir pages use PPR implicitly. Dedicated `ppr-full/` and `ppr/` test suites are mostly `describe.skip` (migrating to cache components). To test PPR codepaths, run normal app-dir e2e tests with `__NEXT_CACHE_COMPONENTS=true` rather than looking for explicit PPR test suites.
- **Quick smoke testing with toy apps**: For fast feedback, generate a minimal test fixture with `pnpm new-test --args true <name> e2e`, then run the dev server directly with `node packages/next/dist/bin/next dev --port <port>` and `curl --max-time 10`. This avoids the overhead of the full test harness and gives immediate feedback on hangs/crashes.
-- **Quick smoke testing with toy apps**: For fast feedback, generate a minimal test fixture with `pnpm new-test -- --args true <name> e2e`, then run the dev server directly with `node packages/next/dist/bin/next dev --port <port>` and `curl --max-time 10`. This avoids the overhead of the full test harness and gives immediate feedback on hangs/crashes.
- Mode-specific tests need `skipStart: true` + manual `next.start()` in `beforeAll` after mode check
- Don't rely on exact log messages - filter by content patterns, find sequences not positions
- **Snapshot tests vary by env flags**: Tests with inline snapshots can produce different output depending on env flags. When updating snapshots, always run the test with the exact env flags the CI job uses (check `.github/workflows/build_and_test.yml` `afterBuild:` sections). Turbopack resolves `react-dom/server.edge` (no Node APIs like `renderToPipeableStream`), while webpack resolves the `.node` build (has them).
Expand Down
3 changes: 3 additions & 0 deletions packages/next/src/client/components/app-router-headers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ export const NEXT_ROUTER_SEGMENT_PREFETCH_HEADER =
'next-router-segment-prefetch' as const
export const NEXT_HMR_REFRESH_HEADER = 'next-hmr-refresh' as const
export const NEXT_HMR_REFRESH_HASH_COOKIE = '__next_hmr_refresh_hash__' as const
// Header used to pass the HMR refresh hash when cookies are unavailable (e.g.
// in cross-origin iframes where SameSite=Lax cookies are not sent).
export const NEXT_HMR_REFRESH_HASH_HEADER = 'next-hmr-refresh-hash' as const
export const NEXT_URL = 'next-url' as const
export const RSC_CONTENT_TYPE_HEADER = 'text/x-component' as const

Expand Down
18 changes: 18 additions & 0 deletions packages/next/src/client/components/hmr-client-state.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
'use client'

// Module-level state for the current HMR refresh hash.
//
// The hash is set by the hot-reloader when server component changes are
// detected, and consumed by fetchServerResponse to ensure cache busting works
// in cross-origin iframe contexts where cookies may not be transmitted due to
// SameSite=Lax restrictions.

let currentHmrRefreshHash: string | undefined

export function setCurrentHmrRefreshHash(hash: string): void {
currentHmrRefreshHash = hash
}

export function getCurrentHmrRefreshHash(): string | undefined {
return currentHmrRefreshHash
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,13 @@ import {
RSC_HEADER,
RSC_CONTENT_TYPE_HEADER,
NEXT_HMR_REFRESH_HEADER,
NEXT_HMR_REFRESH_HASH_HEADER,
NEXT_DID_POSTPONE_HEADER,
NEXT_ROUTER_STALE_TIME_HEADER,
NEXT_HTML_REQUEST_ID_HEADER,
NEXT_REQUEST_ID_HEADER,
} from '../app-router-headers'
import { getCurrentHmrRefreshHash } from '../hmr-client-state'
import { callServer } from '../../app-call-server'
import { findSourceMapURL } from '../../app-find-source-map-url'
import {
Expand Down Expand Up @@ -87,6 +89,7 @@ export type RequestHeaders = {
[NEXT_ROUTER_SEGMENT_PREFETCH_HEADER]?: string
'x-deployment-id'?: string
[NEXT_HMR_REFRESH_HEADER]?: '1'
[NEXT_HMR_REFRESH_HASH_HEADER]?: string
// A header that is only added in test mode to assert on fetch priority
'Next-Test-Fetch-Priority'?: RequestInit['priority']
[NEXT_HTML_REQUEST_ID_HEADER]?: string // dev-only
Expand Down Expand Up @@ -137,6 +140,14 @@ export async function fetchServerResponse(

if (process.env.NODE_ENV === 'development' && options.isHmrRefresh) {
headers[NEXT_HMR_REFRESH_HEADER] = '1'
// Also pass the hash as a header to support cross-origin iframes where
// SameSite=Lax cookies are not sent due to the top-level context being a
// different origin. The server will use the header as a fallback when the
// cookie is absent.
const hmrHash = getCurrentHmrRefreshHash()
if (hmrHash) {
headers[NEXT_HMR_REFRESH_HASH_HEADER] = hmrHash
}
}

if (nextUrl) {
Expand Down
17 changes: 15 additions & 2 deletions packages/next/src/client/dev/hot-reloader/app/hot-reloader-app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import { useUntrackedPathname } from '../../../components/navigation-untracked'
import reportHmrLatency from '../../report-hmr-latency'
import { TurbopackHmr } from '../turbopack-hot-reloader-common'
import { NEXT_HMR_REFRESH_HASH_COOKIE } from '../../../components/app-router-headers'
import { setCurrentHmrRefreshHash } from '../../../components/hmr-client-state'
import {
publicAppRouterInstance,
type GlobalErrorState,
Expand Down Expand Up @@ -407,8 +408,20 @@ export function processMessage(
)

// Store the latest hash in a session cookie so that it's sent back to the
// server with any subsequent requests.
document.cookie = `${NEXT_HMR_REFRESH_HASH_COOKIE}=${message.hash};path=/`
// server with any subsequent requests. Use SameSite=None on HTTPS to allow
// the cookie to be sent from cross-origin iframes (e.g. when using a
// tunnel like cloudflared during development). SameSite=None requires
// Secure, so it can only be set on HTTPS connections.
const cookieFlags =
location.protocol === 'https:'
? `path=/;SameSite=None;Secure`
: `path=/`
document.cookie = `${NEXT_HMR_REFRESH_HASH_COOKIE}=${message.hash};${cookieFlags}`

// Also store in module-level state as a fallback for cross-origin iframe
// contexts where cookies may be blocked (e.g. "Block third-party cookies"
// enabled). The hash will be sent as a request header instead.
setCurrentHmrRefreshHash(message.hash)

if (
RuntimeErrorHandler.hadRuntimeError ||
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,10 @@ import type {
import type { Params } from '../request/params'
import type { ImplicitTags } from '../lib/implicit-tags'
import type { WorkStore } from './work-async-storage.external'
import { NEXT_HMR_REFRESH_HASH_COOKIE } from '../../client/components/app-router-headers'
import {
NEXT_HMR_REFRESH_HASH_COOKIE,
NEXT_HMR_REFRESH_HASH_HEADER,
} from '../../client/components/app-router-headers'
import { InvariantError } from '../../shared/lib/invariant-error'
import type { StagedRenderingController } from './staged-rendering'
import type { ValidationBoundaryTracking } from './instant-validation/boundary-tracking'
Expand Down Expand Up @@ -419,7 +422,14 @@ export function getHmrRefreshHash(
case 'prerender-runtime':
return workUnitStore.hmrRefreshHash
case 'request':
return workUnitStore.cookies.get(NEXT_HMR_REFRESH_HASH_COOKIE)?.value
// Prefer the cookie, but fall back to the request header for cross-origin
// iframe contexts where SameSite=Lax (or blocked third-party cookies)
// prevent the cookie from being sent with the HMR refresh fetch.
return (
workUnitStore.cookies.get(NEXT_HMR_REFRESH_HASH_COOKIE)?.value ??
workUnitStore.headers.get(NEXT_HMR_REFRESH_HASH_HEADER) ??
undefined
)
case 'prerender-client':
case 'validation-client':
case 'prerender-ppr':
Expand Down
Loading