Skip to content

Commit 11c9c92

Browse files
committed
fix(use-cache): fix HMR not updating in cross-origin iframes
1 parent 12d7e60 commit 11c9c92

File tree

5 files changed

+59
-4
lines changed

5 files changed

+59
-4
lines changed

packages/next/src/client/components/app-router-headers.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@ export const NEXT_ROUTER_SEGMENT_PREFETCH_HEADER =
1313
'next-router-segment-prefetch' as const
1414
export const NEXT_HMR_REFRESH_HEADER = 'next-hmr-refresh' as const
1515
export const NEXT_HMR_REFRESH_HASH_COOKIE = '__next_hmr_refresh_hash__' as const
16+
// Header used to pass the HMR refresh hash when cookies are unavailable (e.g.
17+
// in cross-origin iframes where SameSite=Lax cookies are not sent).
18+
export const NEXT_HMR_REFRESH_HASH_HEADER = 'next-hmr-refresh-hash' as const
1619
export const NEXT_URL = 'next-url' as const
1720
export const RSC_CONTENT_TYPE_HEADER = 'text/x-component' as const
1821

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
'use client'
2+
3+
// Module-level state for the current HMR refresh hash.
4+
//
5+
// The hash is set by the hot-reloader when server component changes are
6+
// detected, and consumed by fetchServerResponse to ensure cache busting works
7+
// in cross-origin iframe contexts where cookies may not be transmitted due to
8+
// SameSite=Lax restrictions.
9+
10+
let currentHmrRefreshHash: string | undefined
11+
12+
export function setCurrentHmrRefreshHash(hash: string): void {
13+
currentHmrRefreshHash = hash
14+
}
15+
16+
export function getCurrentHmrRefreshHash(): string | undefined {
17+
return currentHmrRefreshHash
18+
}

packages/next/src/client/components/router-reducer/fetch-server-response.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,11 +22,13 @@ import {
2222
RSC_HEADER,
2323
RSC_CONTENT_TYPE_HEADER,
2424
NEXT_HMR_REFRESH_HEADER,
25+
NEXT_HMR_REFRESH_HASH_HEADER,
2526
NEXT_DID_POSTPONE_HEADER,
2627
NEXT_ROUTER_STALE_TIME_HEADER,
2728
NEXT_HTML_REQUEST_ID_HEADER,
2829
NEXT_REQUEST_ID_HEADER,
2930
} from '../app-router-headers'
31+
import { getCurrentHmrRefreshHash } from '../hmr-client-state'
3032
import { callServer } from '../../app-call-server'
3133
import { findSourceMapURL } from '../../app-find-source-map-url'
3234
import {
@@ -87,6 +89,7 @@ export type RequestHeaders = {
8789
[NEXT_ROUTER_SEGMENT_PREFETCH_HEADER]?: string
8890
'x-deployment-id'?: string
8991
[NEXT_HMR_REFRESH_HEADER]?: '1'
92+
[NEXT_HMR_REFRESH_HASH_HEADER]?: string
9093
// A header that is only added in test mode to assert on fetch priority
9194
'Next-Test-Fetch-Priority'?: RequestInit['priority']
9295
[NEXT_HTML_REQUEST_ID_HEADER]?: string // dev-only
@@ -137,6 +140,14 @@ export async function fetchServerResponse(
137140

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

142153
if (nextUrl) {

packages/next/src/client/dev/hot-reloader/app/hot-reloader-app.tsx

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ import { useUntrackedPathname } from '../../../components/navigation-untracked'
3232
import reportHmrLatency from '../../report-hmr-latency'
3333
import { TurbopackHmr } from '../turbopack-hot-reloader-common'
3434
import { NEXT_HMR_REFRESH_HASH_COOKIE } from '../../../components/app-router-headers'
35+
import { setCurrentHmrRefreshHash } from '../../../components/hmr-client-state'
3536
import {
3637
publicAppRouterInstance,
3738
type GlobalErrorState,
@@ -407,8 +408,20 @@ export function processMessage(
407408
)
408409

409410
// Store the latest hash in a session cookie so that it's sent back to the
410-
// server with any subsequent requests.
411-
document.cookie = `${NEXT_HMR_REFRESH_HASH_COOKIE}=${message.hash};path=/`
411+
// server with any subsequent requests. Use SameSite=None on HTTPS to allow
412+
// the cookie to be sent from cross-origin iframes (e.g. when using a
413+
// tunnel like cloudflared during development). SameSite=None requires
414+
// Secure, so it can only be set on HTTPS connections.
415+
const cookieFlags =
416+
location.protocol === 'https:'
417+
? `path=/;SameSite=None;Secure`
418+
: `path=/`
419+
document.cookie = `${NEXT_HMR_REFRESH_HASH_COOKIE}=${message.hash};${cookieFlags}`
420+
421+
// Also store in module-level state as a fallback for cross-origin iframe
422+
// contexts where cookies may be blocked (e.g. "Block third-party cookies"
423+
// enabled). The hash will be sent as a request header instead.
424+
setCurrentHmrRefreshHash(message.hash)
412425

413426
if (
414427
RuntimeErrorHandler.hadRuntimeError ||

packages/next/src/server/app-render/work-unit-async-storage.external.ts

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,10 @@ import type {
1818
import type { Params } from '../request/params'
1919
import type { ImplicitTags } from '../lib/implicit-tags'
2020
import type { WorkStore } from './work-async-storage.external'
21-
import { NEXT_HMR_REFRESH_HASH_COOKIE } from '../../client/components/app-router-headers'
21+
import {
22+
NEXT_HMR_REFRESH_HASH_COOKIE,
23+
NEXT_HMR_REFRESH_HASH_HEADER,
24+
} from '../../client/components/app-router-headers'
2225
import { InvariantError } from '../../shared/lib/invariant-error'
2326
import type { StagedRenderingController } from './staged-rendering'
2427
import type { ValidationBoundaryTracking } from './instant-validation/boundary-tracking'
@@ -419,7 +422,14 @@ export function getHmrRefreshHash(
419422
case 'prerender-runtime':
420423
return workUnitStore.hmrRefreshHash
421424
case 'request':
422-
return workUnitStore.cookies.get(NEXT_HMR_REFRESH_HASH_COOKIE)?.value
425+
// Prefer the cookie, but fall back to the request header for cross-origin
426+
// iframe contexts where SameSite=Lax (or blocked third-party cookies)
427+
// prevent the cookie from being sent with the HMR refresh fetch.
428+
return (
429+
workUnitStore.cookies.get(NEXT_HMR_REFRESH_HASH_COOKIE)?.value ??
430+
workUnitStore.headers.get(NEXT_HMR_REFRESH_HASH_HEADER) ??
431+
undefined
432+
)
423433
case 'prerender-client':
424434
case 'validation-client':
425435
case 'prerender-ppr':

0 commit comments

Comments
 (0)