From f1a6fb7228b44d31fb10ba6e281b1ae1695b1dbf Mon Sep 17 00:00:00 2001 From: ezefernandezyf Date: Thu, 24 Sep 2026 13:53:36 -0300 Subject: [PATCH] fix: forward custom encode option to cookie serializer cookie v2 split the stringify API into two arguments: stringifySetCookie(cookie: SetCookie, options?: StringifyOptions) `encode` belongs to `StringifyOptions`, while the cookie attributes stay in the first argument. Since the cookie v2.x migration (#361) every option was merged into the first argument, so a custom `encode` was silently dropped and the default encoder was used instead, double encoding the value: `%2F` became `%252F`. Split `encode` out of the attributes and pass it as the second argument. `options || {}` is required because `fastify.serializeCookie(name, value)` is a public call path with an optional options object, and a bare destructuring would throw a TypeError there. Refs: #376 --- index.js | 3 ++- test/cookie.test.js | 40 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 42 insertions(+), 1 deletion(-) diff --git a/index.js b/index.js index a381db7..3906c8b 100644 --- a/index.js +++ b/index.js @@ -13,7 +13,8 @@ async function dynamicLoadCookie () { } function serialize (name, value, options) { - return cookieModule.stringifySetCookie(Object.assign({ name, value }, options)) + const { encode, ...cookieAttributes } = options || {} + return cookieModule.stringifySetCookie(Object.assign({ name, value }, cookieAttributes), { encode }) } function parse (header, options) { diff --git a/test/cookie.test.js b/test/cookie.test.js index 50aba33..e681d69 100644 --- a/test/cookie.test.js +++ b/test/cookie.test.js @@ -763,6 +763,46 @@ test('serialize cookie manually using decorator', async (t) => { t.assert.deepStrictEqual(fastify.serializeCookie('foo', 'bar', {}), 'foo=bar') }) +test('setCookie forwards custom encode option to cookie serializer', async (t) => { + t.plan(2) + + const fastify = Fastify() + fastify.register(plugin) + + fastify.get('/test', (_req, reply) => { + reply.setCookie('test', 'a=%2F&b=c', { encode: String }) + reply.send('done') + }) + + const res = await fastify.inject({ url: '/test' }) + + t.assert.strictEqual(res.statusCode, 200) + t.assert.strictEqual(res.headers['set-cookie'], 'test=a=%2F&b=c; SameSite=Lax') +}) + +test('serialize forwards custom encode option', async (t) => { + t.plan(2) + + const fastify = Fastify() + fastify.register(plugin) + + await new Promise(resolve => fastify.ready(resolve)) + + t.assert.strictEqual(plugin.serialize('test', 'a=%2F&b=c', { encode: String }), 'test=a=%2F&b=c') + t.assert.strictEqual(fastify.serializeCookie('test', 'a=%2F&b=c', { encode: String }), 'test=a=%2F&b=c') +}) + +test('serializeCookie does not require an options object', async (t) => { + t.plan(1) + + const fastify = Fastify() + fastify.register(plugin) + + await new Promise(resolve => fastify.ready(resolve)) + + t.assert.strictEqual(fastify.serializeCookie('foo', 'bar'), 'foo=bar') +}) + test('parse cookie manually using decorator', async (t) => { t.plan(2)