graphql-yoga
graphql-yoga copied to clipboard
When defining multiple cookies, they are all sent in a single `set-cookie` header
Describe the bug
Within my lambda, I define multiple cookies when a user logged in: Token
& UserInfo
. I use the cookieStore
from the context to define them.
It works perfectly locally (when using createServer
) both cookie are sent in a single set-cookie
headers:
set-cookie: Token=JWTOKENblablabla; Domain=mydomain.me; Path=/; Expires=Fri, 17 Nov 2023 10:28:13 GMT; SameSite=Lax; HttpOnly
set-cookie: UserInfo=somedata; Domain=mydomain.me; Path=/; Expires=Fri, 17 Nov 2023 10:28:13 GMT; SameSite=Lax
But when I deploy the code to AWS (I'm using an API G REST, not HTTP) only one header set-cookie
is sent with both cookies inside. So of course, the behavior isn't met on the client side:
set-cookie: Token=JWTOKENblablabla; Domain=mydomain.me; Path=/; Expires=Fri, 17 Nov 2023 09:02:41 GMT; SameSite=Lax; HttpOnly, UserInfo=somedata; Domain=mydomain.me; Path=/; Expires=Fri, 17 Nov 2023 09:02:41 GMT; SameSite=Lax
I'm using the Lambda integration as defined in that example: https://the-guild.dev/graphql/yoga-server/docs/integrations/integration-with-aws-lambda
Your Example Website or App
https://github.com/j0k3r/repro-yoga-cookies
Steps to Reproduce the Bug or Issue
It's hard to provide a sample code reproduction because the code must be deployed to AWS. But I create a small project and deployed it.
handler1
with the bug
handler1
defines 2 cookies:
import { createSchema, createYoga } from 'graphql-yoga'
import { useCookies } from '@whatwg-node/server-plugin-cookies'
const yoga = createYoga({
logging: true,
maskedErrors: false,
graphqlEndpoint: '/handler1',
schema: createSchema({
typeDefs: /* GraphQL */ `
type Query {
greetings: String
}
`,
resolvers: {
Query: {
async greetings (root, args, ctx) {
await ctx.request.cookieStore?.set('Token', 'JWTOKENblablabla')
await ctx.request.cookieStore?.set('UserInfo', 'somedata')
return 'This is the `greetings` field of the root `Query` type'
}
}
}
}),
plugins: [useCookies()]
})
export async function handler(event, lambdaContext) {
const response = await yoga.fetch(
event.path +
'?' +
new URLSearchParams((event.queryStringParameters) || {}).toString(),
{
method: event.httpMethod,
headers: event.headers,
body: event.body
? Buffer.from(event.body, event.isBase64Encoded ? 'base64' : 'utf8')
: undefined
},
{
event,
lambdaContext
}
)
const responseHeaders = Object.fromEntries(response.headers.entries())
return {
statusCode: response.status,
headers: responseHeaders,
body: await response.text(),
isBase64Encoded: false
}
}
It can be requested using:
curl -i \
--url https://gewtxb5z00.execute-api.eu-west-1.amazonaws.com/dev/handler1 \
--header 'Content-Type: application/json' \
--data '{"query":"query hello { greetings }","operationName":"hello"}'
In the response, you'll have only one set-cookie
with both cookies define inside:
HTTP/2 200
content-type: application/json; charset=utf-8
content-length: 79
date: Wed, 18 Oct 2023 11:21:00 GMT
x-amzn-requestid: 4f5580bd-914c-441e-a93f-121e20b7e2ea
x-amzn-remapped-content-length: 79
set-cookie: Token=JWTOKENblablabla; Path=/; SameSite=Strict, UserInfo=somedata; Path=/; SameSite=Strict
x-amz-apigw-id: M_rgcHz2joEEJ6A=
x-amzn-trace-id: Root=1-652fbf9c-1c59373166eddd11621cacd4;Sampled=0;lineage=f3deedb7:0
x-cache: Miss from cloudfront
via: 1.1 bfd596aba0de57f83442d2ebd6b268f4.cloudfront.net (CloudFront)
x-amz-cf-pop: CDG52-P1
x-amz-cf-id: VxQaFyztVir4OwVnSpR0DAVhEB_wm7SjuiNEtlkio57n31RJuhXjlA==
{"data":{"greetings":"This is the `greetings` field of the root `Query` type"}}
handler2
with an ugly workaround
I found a workaround to solve my problem. I re-defined useCookies
to set a header Set-Cookies
with cookies JSON stringified inside. Then, when sending the response, I used these stringified cookies to defined the proper key multiValueHeaders
used by API G to handle (at least) multiple cookies definition.
import { createSchema, createYoga } from 'graphql-yoga'
import { CookieStore, getCookieString } from '@whatwg-node/cookie-store'
const useCustomCookies = () => {
const cookieStringsByRequest = new WeakMap()
return {
onRequest({ request }) {
const cookieStrings = []
request.cookieStore = new CookieStore(request.headers.get('cookie') ?? '')
request.cookieStore.onchange = ({ changed, deleted }) => {
changed.forEach((cookie) => {
cookieStrings.push(getCookieString(cookie))
})
deleted.forEach((cookie) => {
cookieStrings.push(getCookieString({ ...cookie, value: undefined }))
})
}
cookieStringsByRequest.set(request, cookieStrings)
},
onResponse({ request, response }) {
const cookieStrings = cookieStringsByRequest.get(request)
cookieStrings?.forEach((cookieString) => {
response.headers.append('Set-Cookie', cookieString)
})
// that the only line added
response.headers.set('Set-Cookies', JSON.stringify(cookieStrings))
},
}
}
const yoga = createYoga({
logging: true,
maskedErrors: false,
graphqlEndpoint: '/handler2',
schema: createSchema({
typeDefs: /* GraphQL */ `
type Query {
greetings: String
}
`,
resolvers: {
Query: {
async greetings (root, args, ctx) {
await ctx.request.cookieStore?.set('Token', 'JWTOKENblablabla')
await ctx.request.cookieStore?.set('UserInfo', 'somedata')
return 'This is the `greetings` field of the root `Query` type'
}
}
}
}),
plugins: [useCustomCookies()]
})
export async function handler(event, lambdaContext) {
const response = await yoga.fetch(
event.path +
'?' +
new URLSearchParams((event.queryStringParameters) || {}).toString(),
{
method: event.httpMethod,
headers: event.headers,
body: event.body
? Buffer.from(event.body, event.isBase64Encoded ? 'base64' : 'utf8')
: undefined
},
{
event,
lambdaContext
}
)
const multiValueHeaders = {}
const headers = Object.fromEntries(response.headers.entries())
if (headers['set-cookies']) {
multiValueHeaders['set-cookie'] = JSON.parse(headers['set-cookies'])
delete headers['set-cookies']
delete headers['set-cookie']
}
return {
statusCode: response.status,
headers,
multiValueHeaders,
body: await response.text(),
isBase64Encoded: false,
}
}
It can be requested using:
curl -i \
--url https://gewtxb5z00.execute-api.eu-west-1.amazonaws.com/dev/handler2 \
--header 'Content-Type: application/json' \
--data '{"query":"query hello { greetings }","operationName":"hello"}'
In the response, you'll have two set-cookie
header for each cookie:
HTTP/2 200
content-type: application/json; charset=utf-8
content-length: 79
date: Wed, 18 Oct 2023 11:23:59 GMT
x-amzn-requestid: 35532f3c-b45b-4081-b857-2352a7b499e1
x-amzn-remapped-content-length: 79
set-cookie: Token=JWTOKENblablabla; Path=/; SameSite=Strict
set-cookie: UserInfo=somedata; Path=/; SameSite=Strict
x-amz-apigw-id: M_r8XGR5DoEECBg=
x-amzn-trace-id: Root=1-652fc04e-1ddb0e0d4661a4b85133ce7e;Sampled=0;lineage=869de0a0:0
x-cache: Miss from cloudfront
via: 1.1 a769201928d4a671d76c2aeb231718ae.cloudfront.net (CloudFront)
x-amz-cf-pop: CDG52-P1
x-amz-cf-id: ZHEFG9JsCQmFacJOlAidhY-NC9CCo5eom4o_Da064Zeo_12TbUe0aQ==
{"data":{"greetings":"This is the `greetings` field of the root `Query` type"}}
Expected behavior
As a user, I want to receive two cookies.
Screenshots or Videos
No response
Platform
- NodeJS: 18
- Package info:
{ "dependencies": { "@whatwg-node/cookie-store": "^0.2.2", "@whatwg-node/server": "^0.9.14", "@whatwg-node/server-plugin-cookies": "^1.0.2", "graphql": "^16.8.1", "graphql-yoga": "^5.0.0", "source-map-support": "^0.5.21" } }
Additional context
No response