Skip to content

fix(headers): align draft mode cookie attributes with Next.js#992

Merged
james-elicx merged 1 commit intocloudflare:mainfrom
NathanDrake2406:nathan/fix-draft-mode-cookie-attrs
Apr 30, 2026
Merged

fix(headers): align draft mode cookie attributes with Next.js#992
james-elicx merged 1 commit intocloudflare:mainfrom
NathanDrake2406:nathan/fix-draft-mode-cookie-attrs

Conversation

@NathanDrake2406
Copy link
Copy Markdown
Contributor

What this changes

Vinext draft-mode cookies now match Next.js production cookie semantics. draftMode().enable() emits SameSite=None; Secure outside development, and draftMode().disable() clears the bypass cookie with an expired date while preserving the same production attributes.

Why

The existing shim always emitted SameSite=Lax and cleared with Max-Age=0. That diverges from Next.js and can prevent production preview flows from receiving the __prerender_bypass cookie in cross-site contexts, leaving draftMode().isEnabled false even after enabling draft mode.

Next.js references:

  • App Router DraftModeProvider.enable() uses sameSite: process.env.NODE_ENV !== 'development' ? 'none' : 'lax' and secure: process.env.NODE_ENV !== 'development': draft-mode-provider.ts#L68-L75
  • App Router DraftModeProvider.disable() clears via expires: new Date(0) with the same cookie attributes: draft-mode-provider.ts#L80-L92
  • Pages/API preview helpers use the same production sameSite and secure policy: api-resolver.ts#L131-L160

Approach

The draft-mode cookie behavior stays in the normal next/headers shim module. Generated entries still only ask the shim for the pending draft-mode cookie header, so codegen remains app-shape wiring rather than owning cookie policy.

The implementation adds a small helper for draft-mode cookie attributes:

  • development: Path=/; HttpOnly; SameSite=Lax
  • non-development: Path=/; HttpOnly; SameSite=None; Secure

Disable now emits Expires=Thu, 01 Jan 1970 00:00:00 GMT instead of Max-Age=0, matching Next.js's documented source comment and avoiding a cookie-clear form Next.js explicitly does not use.

Validation

  • vp test run tests/nextjs-compat/draft-mode.test.ts -t "cross-site|Next.js production cookie attributes" failed before the fix with SameSite=Lax and Max-Age=0.
  • vp test run tests/nextjs-compat/draft-mode.test.ts
  • vp test run tests/shims.test.ts -t "draftMode"
  • vp lint
  • vp fmt
  • vp check

Risks / follow-ups

This intentionally changes only draft-mode cookie serialization. Request parsing, secret validation, generated entry wiring, and route-handler response plumbing are unchanged.

@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new Bot commented Apr 30, 2026

Open in StackBlitz

npm i https://pkg.pr.new/vinext@992

commit: 4b1da5d

@NathanDrake2406 NathanDrake2406 marked this pull request as ready for review April 30, 2026 10:46
Copilot AI review requested due to automatic review settings April 30, 2026 10:46
Draft mode cookies currently use SameSite=Lax in production and clear with Max-Age=0. That diverges from Next.js and can prevent production preview flows from receiving the bypass cookie in cross-site contexts.

Centralize draft-mode cookie attributes in the headers shim, use SameSite=None and Secure outside development, and clear the bypass cookie with an expired date. Add focused compatibility coverage for production enable and disable attributes.
@NathanDrake2406 NathanDrake2406 force-pushed the nathan/fix-draft-mode-cookie-attrs branch from 945e73a to 4b1da5d Compare April 30, 2026 10:50
@james-elicx
Copy link
Copy Markdown
Collaborator

/bigbonk review

Copy link
Copy Markdown
Contributor

@ask-bonk ask-bonk Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Clean, well-scoped fix. The cookie attribute changes match the linked Next.js source exactly, the helper consolidates the duplicated typeof process guard, and the tests cover both enable() and disable() in production mode.

One minor nit below — not blocking.


// Draft mode cookie name (matches Next.js convention)
const DRAFT_MODE_COOKIE = "__prerender_bypass";
const DRAFT_MODE_EXPIRED_DATE = new Date(0).toUTCString();
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: EXPIRED_COOKIE_DATE at line 64 and DRAFT_MODE_EXPIRED_DATE here are the same value (new Date(0).toUTCString()). Consider reusing the existing constant instead of introducing a second one.

Suggested change
const DRAFT_MODE_EXPIRED_DATE = new Date(0).toUTCString();
const DRAFT_MODE_EXPIRED_DATE = EXPIRED_COOKIE_DATE;

Or just use EXPIRED_COOKIE_DATE directly at the call site on line 771. Not blocking — the duplication is harmless and both are module-scoped constants.

}
});

it("draftMode().enable() uses cross-site cookie attributes in production", async () => {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: This test is almost identical to the existing "includes Secure flag in production" test at line 79 — both set NODE_ENV=production, call enable(), and assert Secure. The only new assertion here is SameSite=None. Consider folding the SameSite=None assertion into the existing test rather than duplicating the entire setup. Keeps the test file tighter and avoids two NODE_ENV swaps for the same code path.

@ask-bonk
Copy link
Copy Markdown
Contributor

ask-bonk Bot commented Apr 30, 2026

Review submitted. Here's a summary:

Approved with two minor nits:

  1. Duplicate constant (headers.ts:690): DRAFT_MODE_EXPIRED_DATE is identical to the existing EXPIRED_COOKIE_DATE at line 64. Suggested reusing the existing constant instead of introducing a second new Date(0).toUTCString().

  2. Near-duplicate test (draft-mode.test.ts:103): The new "cross-site cookie attributes" test is nearly identical to the existing "Secure flag in production" test at line 79 — both set NODE_ENV=production, call enable(), and assert Secure. Suggested folding the SameSite=None assertion into the existing test to avoid duplicating the setup.

The core change is correct and well-motivated — SameSite=None; Secure in production and Expires instead of Max-Age=0 for disable both match the linked Next.js source precisely.

github run

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.

@james-elicx james-elicx merged commit f367f30 into cloudflare:main Apr 30, 2026
26 checks passed
@NathanDrake2406 NathanDrake2406 deleted the nathan/fix-draft-mode-cookie-attrs branch May 6, 2026 04:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants