-
Notifications
You must be signed in to change notification settings - Fork 30k
prevent cookies from being set in the wrong phase #71094
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
ab95c4c
28a6d29
7e69edc
6cde6df
f5def7e
0ede32a
f69c3d9
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -13,6 +13,8 @@ import { | |
| import { | ||
| MutableRequestCookiesAdapter, | ||
| RequestCookiesAdapter, | ||
| responseCookiesToRequestCookies, | ||
| wrapWithMutableAccessCheck, | ||
| type ReadonlyRequestCookies, | ||
| } from '../web/spec-extension/adapters/request-cookies' | ||
| import { ResponseCookies, RequestCookies } from '../web/spec-extension/cookies' | ||
|
|
@@ -165,6 +167,7 @@ function createRequestStoreImpl( | |
| headers?: ReadonlyHeaders | ||
| cookies?: ReadonlyRequestCookies | ||
| mutableCookies?: ResponseCookies | ||
| userspaceMutableCookies?: ResponseCookies | ||
| draftMode?: DraftModeProvider | ||
| } = {} | ||
|
|
||
|
|
@@ -202,6 +205,9 @@ function createRequestStoreImpl( | |
|
|
||
| return cache.cookies | ||
| }, | ||
| set cookies(value: ReadonlyRequestCookies) { | ||
| cache.cookies = value | ||
| }, | ||
| get mutableCookies() { | ||
| if (!cache.mutableCookies) { | ||
| const mutableCookies = getMutableCookies( | ||
|
|
@@ -215,6 +221,15 @@ function createRequestStoreImpl( | |
| } | ||
| return cache.mutableCookies | ||
| }, | ||
| get userspaceMutableCookies() { | ||
| if (!cache.userspaceMutableCookies) { | ||
| const userspaceMutableCookies = wrapWithMutableAccessCheck( | ||
| this.mutableCookies | ||
| ) | ||
| cache.userspaceMutableCookies = userspaceMutableCookies | ||
| } | ||
| return cache.userspaceMutableCookies | ||
| }, | ||
| get draftMode() { | ||
| if (!cache.draftMode) { | ||
| cache.draftMode = new DraftModeProvider( | ||
|
|
@@ -234,3 +249,10 @@ function createRequestStoreImpl( | |
| (globalThis as any).__serverComponentsHmrCache, | ||
| } | ||
| } | ||
|
|
||
| export function synchronizeMutableCookies(store: RequestStore) { | ||
| // TODO: does this need to update headers as well? | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If you're updating the cookies here, you don't need to worry about the headers as they aren't mutable.
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. right, but i think the render that follows the action won't see the
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. to explain it better. the point here is that when we run an action async function action() {
"use server"
;(await cookies()).set("myCookie", "1")
}and we also render the updated version of the page, then that page render should see that cookie value: (await cookies()).get("myCookie").value === '1'but now i'm wondering if the same applies to headers? should the render see |
||
| store.cookies = RequestCookiesAdapter.seal( | ||
| responseCookiesToRequestCookies(store.mutableCookies) | ||
| ) | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,8 +1,12 @@ | ||
| import type { RequestCookies } from '../cookies' | ||
| import { RequestCookies } from '../cookies' | ||
|
|
||
| import { ResponseCookies } from '../cookies' | ||
| import { ReflectAdapter } from './reflect' | ||
| import { workAsyncStorage } from '../../../app-render/work-async-storage.external' | ||
| import { | ||
| getExpectedRequestStore, | ||
| type RequestStore, | ||
| } from '../../../app-render/work-unit-async-storage.external' | ||
|
|
||
| /** | ||
| * @internal | ||
|
|
@@ -63,6 +67,10 @@ export function getModifiedCookieValues( | |
| return modified | ||
| } | ||
|
|
||
| type SetCookieArgs = | ||
| | [key: string, value: string, cookie?: Partial<ResponseCookie>] | ||
| | [options: ResponseCookie] | ||
|
|
||
| export function appendMutableCookies( | ||
| headers: Headers, | ||
| mutableCookies: ResponseCookies | ||
|
|
@@ -128,7 +136,7 @@ export class MutableRequestCookiesAdapter { | |
| } | ||
| } | ||
|
|
||
| return new Proxy(responseCookies, { | ||
| const wrappedCookies = new Proxy(responseCookies, { | ||
| get(target, prop, receiver) { | ||
| switch (prop) { | ||
| // A special symbol to get the modified cookie values | ||
|
|
@@ -144,29 +152,86 @@ export class MutableRequestCookiesAdapter { | |
| ) | ||
| try { | ||
| target.delete(...args) | ||
| return wrappedCookies | ||
|
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. small bugfix -- the type signature of ResponseCookies says that |
||
| } finally { | ||
| updateResponseCookies() | ||
| } | ||
| } | ||
| case 'set': | ||
| return function ( | ||
| ...args: | ||
| | [key: string, value: string, cookie?: Partial<ResponseCookie>] | ||
| | [options: ResponseCookie] | ||
| ) { | ||
| return function (...args: SetCookieArgs) { | ||
| modifiedCookies.add( | ||
| typeof args[0] === 'string' ? args[0] : args[0].name | ||
| ) | ||
| try { | ||
| return target.set(...args) | ||
| target.set(...args) | ||
| return wrappedCookies | ||
|
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. and another small bugfix: if someone did |
||
| } finally { | ||
| updateResponseCookies() | ||
| } | ||
| } | ||
|
|
||
| default: | ||
| return ReflectAdapter.get(target, prop, receiver) | ||
| } | ||
| }, | ||
| }) | ||
|
|
||
| return wrappedCookies | ||
| } | ||
| } | ||
|
|
||
| export function wrapWithMutableAccessCheck( | ||
|
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. it's not ideal to have to add another proxy. i tried a different approach originally (see the "fix: setting cookies in draftmode" commit), but that felt like it leaks this all over the place (needed a custom |
||
| responseCookies: ResponseCookies | ||
| ): ResponseCookies { | ||
| const wrappedCookies = new Proxy(responseCookies, { | ||
| get(target, prop, receiver) { | ||
| switch (prop) { | ||
| case 'delete': | ||
| return function (...args: [string] | [ResponseCookie]) { | ||
| ensureCookiesAreStillMutable('cookies().delete') | ||
| target.delete(...args) | ||
| return wrappedCookies | ||
lubieowoce marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| } | ||
| case 'set': | ||
| return function (...args: SetCookieArgs) { | ||
| ensureCookiesAreStillMutable('cookies().set') | ||
| target.set(...args) | ||
| return wrappedCookies | ||
| } | ||
|
|
||
| default: | ||
| return ReflectAdapter.get(target, prop, receiver) | ||
| } | ||
| }, | ||
| }) | ||
| return wrappedCookies | ||
| } | ||
|
|
||
| export function areCookiesMutableInCurrentPhase(requestStore: RequestStore) { | ||
| return requestStore.phase === 'action' | ||
| } | ||
|
|
||
| /** Ensure that cookies() starts throwing on mutation | ||
| * if we changed phases and can no longer mutate. | ||
| * | ||
| * This can happen when going: | ||
| * 'render' -> 'after' | ||
| * 'action' -> 'render' | ||
| * */ | ||
| function ensureCookiesAreStillMutable(callingExpression: string) { | ||
| const requestStore = getExpectedRequestStore(callingExpression) | ||
| if (!areCookiesMutableInCurrentPhase(requestStore)) { | ||
| // TODO: maybe we can give a more precise error message based on callingExpression? | ||
| throw new ReadonlyRequestCookiesError() | ||
| } | ||
| } | ||
|
|
||
| export function responseCookiesToRequestCookies( | ||
| responseCookies: ResponseCookies | ||
| ): RequestCookies { | ||
| const requestCookies = new RequestCookies(new Headers()) | ||
| for (const cookie of responseCookies.getAll()) { | ||
| requestCookies.set(cookie) | ||
| } | ||
| return requestCookies | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this seems strange to me at first glance -- ReadonlyRequestCookies type no longer being readonly. I'm sure there's a good reason but probably worth a comment
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this is for the
synchronizeMutableCookiesthing i added -- i need to reassign it to provide the updated version of the cookies to the render after an action executes.(we didn't need to do this before because we were incorrectly giving the render
requestStore.mutableCookies).i added a similar comment to explain, pls check if the explanation makes sense