Skip to content

Commit 30668e6

Browse files
authored
fix(HttpResponse): forward cookies only when response is used (#2728)
1 parent a680221 commit 30668e6

4 files changed

Lines changed: 50 additions & 26 deletions

File tree

src/core/handlers/RequestHandler.ts

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { Headers as HeadersPolyfill } from 'headers-polyfill'
12
import { getCallFrame } from '../utils/internal/getCallFrame'
23
import {
34
isIterable,
@@ -12,6 +13,7 @@ import {
1213
type DefaultUnsafeFetchResponse,
1314
} from '../HttpResponse'
1415
import type { GraphQLRequestBody } from './GraphQLHandler'
16+
import { getRawSetCookie } from '../utils/HttpResponse/decorators'
1517

1618
export type DefaultRequestMultipartBody = Record<
1719
string,
@@ -335,7 +337,7 @@ export abstract class RequestHandler<
335337
...resolverExtras,
336338
requestId: args.requestId,
337339
request: args.request,
338-
}) as Promise<Response>
340+
}) as Promise<Response | undefined>
339341
).catch((errorOrResponse) => {
340342
// Allow throwing a Response instance in a response resolver.
341343
if (errorOrResponse instanceof Response) {
@@ -348,6 +350,10 @@ export abstract class RequestHandler<
348350

349351
const mockedResponse = await mockedResponsePromise
350352

353+
if (mockedResponse) {
354+
forwardResponseCookies(mockedResponse)
355+
}
356+
351357
const executionResult = this.createExecutionResult({
352358
// Pass the cloned request to the result so that logging
353359
// and other consumers could read its body once more.
@@ -416,3 +422,32 @@ export abstract class RequestHandler<
416422
}
417423
}
418424
}
425+
426+
/**
427+
* Forwards the cookies from the given response to `document.cookie`.
428+
*/
429+
export function forwardResponseCookies(response: Response): void {
430+
// Cookie forwarding is only relevant in the browser.
431+
if (typeof document === 'undefined') {
432+
return
433+
}
434+
435+
const responseCookies = getRawSetCookie(response)
436+
437+
if (!responseCookies) {
438+
return
439+
}
440+
441+
// Write the mocked response cookies to the document.
442+
// Use `headers-polyfill` to get the Set-Cookie header value correctly.
443+
// This is an alternative until TypeScript 5.2
444+
// and Node.js v20 become the minimum supported versions
445+
// and "Headers.prototype.getSetCookie" can be used directly.
446+
const allResponseCookies = HeadersPolyfill.prototype.getSetCookie.call(
447+
new Headers([['set-cookie', responseCookies]]),
448+
)
449+
450+
for (const cookieString of allResponseCookies) {
451+
document.cookie = cookieString
452+
}
453+
}

src/core/utils/HttpResponse/decorators.ts

Lines changed: 6 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,9 @@
11
import statuses from '../../../shims/statuses'
2-
import { Headers as HeadersPolyfill } from 'headers-polyfill'
32
import type { HttpResponseInit } from '../../HttpResponse'
43

54
const { message } = statuses
65

7-
export const kSetCookie = Symbol('kSetCookie')
6+
const kSetCookie = Symbol('kSetCookie')
87

98
export interface HttpResponseDecoratedInit extends HttpResponseInit {
109
status: number
@@ -31,7 +30,7 @@ export function decorateResponse(
3130
response: Response,
3231
init: HttpResponseDecoratedInit,
3332
): Response {
34-
// Allow to mock the response type.
33+
// Allow mocking the response type.
3534
if (init.type) {
3635
Object.defineProperty(response, 'type', {
3736
value: init.type,
@@ -52,25 +51,11 @@ export function decorateResponse(
5251
enumerable: false,
5352
writable: false,
5453
})
55-
56-
// Cookie forwarding is only relevant in the browser.
57-
if (typeof document !== 'undefined') {
58-
// Write the mocked response cookies to the document.
59-
// Use `headers-polyfill` to get the Set-Cookie header value correctly.
60-
// This is an alternative until TypeScript 5.2
61-
// and Node.js v20 become the minimum supported version
62-
// and getSetCookie in Headers can be used directly.
63-
const responseCookiePairs = HeadersPolyfill.prototype.getSetCookie.call(
64-
init.headers,
65-
)
66-
67-
for (const cookieString of responseCookiePairs) {
68-
// No need to parse the cookie headers because it's defined
69-
// as the valid cookie string to begin with.
70-
document.cookie = cookieString
71-
}
72-
}
7354
}
7455

7556
return response
7657
}
58+
59+
export function getRawSetCookie(response: Response): string | undefined {
60+
return Reflect.get(response, kSetCookie)
61+
}

src/core/utils/request/storeResponseCookies.ts

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,13 @@
11
import { cookieStore } from '../cookieStore'
2-
import { kSetCookie } from '../HttpResponse/decorators'
2+
import { getRawSetCookie } from '../HttpResponse/decorators'
33

44
export async function storeResponseCookies(
55
request: Request,
66
response: Response,
77
): Promise<void> {
88
// Grab the raw "Set-Cookie" response header provided
99
// in the HeadersInit for this mocked response.
10-
const responseCookies = Reflect.get(response, kSetCookie) as
11-
| string
12-
| undefined
10+
const responseCookies = getRawSetCookie(response)
1311

1412
if (responseCookies) {
1513
await cookieStore.setCookie(responseCookies, request.url)

test/browser/rest-api/request/request-cookies.mocks.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,12 @@ const worker = setupWorker(
66
return HttpResponse.json(cookies)
77
}),
88
http.post('/set-cookies', async ({ request }) => {
9+
new HttpResponse(null, {
10+
headers: {
11+
'Set-Cookie': 'must-not=be-set',
12+
},
13+
})
14+
915
return new HttpResponse(null, {
1016
headers: {
1117
'Set-Cookie': await request.clone().text(),

0 commit comments

Comments
 (0)