Skip to content

Commit f6fb731

Browse files
committed
fix: prevent sending set-cookie header when cookie value is unchanged (#1556)
1 parent 78f4828 commit f6fb731

File tree

3 files changed

+177
-22
lines changed

3 files changed

+177
-22
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
# 1.4.17 - TBD
2+
Bug fix:
3+
- [#1556](https://github.com/elysiajs/elysia/issues/1556) prevent sending set-cookie header when cookie value is not modified
4+
15
# 1.4.16 - 13 Nov 2025
26
Improvement:
37
- ValidationError: add `messageValue` as an alias of `errorValue`

src/cookies.ts

Lines changed: 64 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -142,7 +142,7 @@ export class Cookie<T> implements ElysiaCookie {
142142
}
143143

144144
protected get setCookie() {
145-
if (!(this.name in this.jar)) this.jar[this.name] = this.initial
145+
if (!(this.name in this.jar)) this.jar[this.name] = { ...this.initial }
146146

147147
return this.jar[this.name]
148148
}
@@ -156,87 +156,129 @@ export class Cookie<T> implements ElysiaCookie {
156156
}
157157

158158
set value(value: T) {
159-
this.setCookie.value = value
159+
// Check if value actually changed before creating entry in jar
160+
const current = this.cookie.value
161+
162+
// Simple equality check
163+
if (current === value) return
164+
165+
// For objects, do a deep equality check
166+
if (
167+
typeof current === 'object' &&
168+
current !== null &&
169+
typeof value === 'object' &&
170+
value !== null
171+
) {
172+
try {
173+
if (JSON.stringify(current) === JSON.stringify(value)) return
174+
} catch {
175+
// If stringify fails, proceed with setting the value
176+
}
177+
}
178+
179+
// Only create entry in jar if value actually changed
180+
if (!(this.name in this.jar)) this.jar[this.name] = { ...this.initial }
181+
this.jar[this.name].value = value
160182
}
161183

162184
get expires() {
163185
return this.cookie.expires
164186
}
165187

166-
set expires(expires) {
167-
this.setCookie.expires = expires
188+
set expires(expires: Date | undefined) {
189+
if (this.cookie.expires === expires) return
190+
if (!(this.name in this.jar)) this.jar[this.name] = { ...this.initial }
191+
this.jar[this.name].expires = expires
168192
}
169193

170194
get maxAge() {
171195
return this.cookie.maxAge
172196
}
173197

174-
set maxAge(maxAge) {
175-
this.setCookie.maxAge = maxAge
198+
set maxAge(maxAge: number | undefined) {
199+
if (this.cookie.maxAge === maxAge) return
200+
if (!(this.name in this.jar)) this.jar[this.name] = { ...this.initial }
201+
this.jar[this.name].maxAge = maxAge
176202
}
177203

178204
get domain() {
179205
return this.cookie.domain
180206
}
181207

182-
set domain(domain) {
183-
this.setCookie.domain = domain
208+
set domain(domain: string | undefined) {
209+
if (this.cookie.domain === domain) return
210+
if (!(this.name in this.jar)) this.jar[this.name] = { ...this.initial }
211+
this.jar[this.name].domain = domain
184212
}
185213

186214
get path() {
187215
return this.cookie.path
188216
}
189217

190-
set path(path) {
191-
this.setCookie.path = path
218+
set path(path: string | undefined) {
219+
if (this.cookie.path === path) return
220+
if (!(this.name in this.jar)) this.jar[this.name] = { ...this.initial }
221+
this.jar[this.name].path = path
192222
}
193223

194224
get secure() {
195225
return this.cookie.secure
196226
}
197227

198-
set secure(secure) {
199-
this.setCookie.secure = secure
228+
set secure(secure: boolean | undefined) {
229+
if (this.cookie.secure === secure) return
230+
if (!(this.name in this.jar)) this.jar[this.name] = { ...this.initial }
231+
this.jar[this.name].secure = secure
200232
}
201233

202234
get httpOnly() {
203235
return this.cookie.httpOnly
204236
}
205237

206-
set httpOnly(httpOnly) {
207-
this.setCookie.httpOnly = httpOnly
238+
set httpOnly(httpOnly: boolean | undefined) {
239+
if (this.cookie.httpOnly === httpOnly) return
240+
if (!(this.name in this.jar)) this.jar[this.name] = { ...this.initial }
241+
this.jar[this.name].httpOnly = httpOnly
208242
}
209243

210244
get sameSite() {
211245
return this.cookie.sameSite
212246
}
213247

214-
set sameSite(sameSite) {
215-
this.setCookie.sameSite = sameSite
248+
set sameSite(sameSite: true | false | 'lax' | 'strict' | 'none' | undefined) {
249+
if (this.cookie.sameSite === sameSite) return
250+
if (!(this.name in this.jar)) this.jar[this.name] = { ...this.initial }
251+
this.jar[this.name].sameSite = sameSite
216252
}
217253

218254
get priority() {
219255
return this.cookie.priority
220256
}
221257

222-
set priority(priority) {
223-
this.setCookie.priority = priority
258+
set priority(priority: 'low' | 'medium' | 'high' | undefined) {
259+
if (this.cookie.priority === priority) return
260+
if (!(this.name in this.jar)) this.jar[this.name] = { ...this.initial }
261+
this.jar[this.name].priority = priority
224262
}
225263

226264
get partitioned() {
227265
return this.cookie.partitioned
228266
}
229267

230-
set partitioned(partitioned) {
231-
this.setCookie.partitioned = partitioned
268+
set partitioned(partitioned: boolean | undefined) {
269+
if (this.cookie.partitioned === partitioned) return
270+
if (!(this.name in this.jar)) this.jar[this.name] = { ...this.initial }
271+
this.jar[this.name].partitioned = partitioned
232272
}
233273

234274
get secrets() {
235275
return this.cookie.secrets
236276
}
237277

238-
set secrets(secrets) {
239-
this.setCookie.secrets = secrets
278+
set secrets(secrets: string | string[] | undefined) {
279+
if (this.cookie.secrets === secrets) return
280+
if (!(this.name in this.jar)) this.jar[this.name] = { ...this.initial }
281+
this.jar[this.name].secrets = secrets
240282
}
241283

242284
update(config: Updater<Partial<ElysiaCookie>>) {

test/cookie/unchanged.test.ts

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
import { describe, it, expect } from 'bun:test'
2+
import { Elysia, t } from '../../src'
3+
4+
describe('Cookie - Unchanged Values', () => {
5+
it('should not send set-cookie header when cookie is only read', async () => {
6+
const app = new Elysia()
7+
.guard({
8+
cookie: t.Cookie({
9+
value: t.Optional(
10+
t.Object({
11+
a: t.String(),
12+
b: t.String()
13+
})
14+
)
15+
})
16+
})
17+
.get('/cookie', ({ cookie: { value } }) => value.value)
18+
.post('/cookie', ({ cookie: { value } }) => {
19+
value.value = { a: '1', b: '2' }
20+
return 'ok'
21+
})
22+
23+
// POST request should set cookie
24+
const postResponse = await app.handle(
25+
new Request('http://localhost/cookie', {
26+
method: 'POST'
27+
})
28+
)
29+
30+
const setCookieHeaders = postResponse.headers.getAll('set-cookie')
31+
expect(setCookieHeaders.length).toBeGreaterThan(0)
32+
33+
// GET request should NOT set cookie (only reading)
34+
const getResponse = await app.handle(
35+
new Request('http://localhost/cookie', {
36+
method: 'GET',
37+
headers: {
38+
cookie: setCookieHeaders[0].split(';')[0]
39+
}
40+
})
41+
)
42+
43+
const getSetCookieHeaders = getResponse.headers.getAll('set-cookie')
44+
expect(getSetCookieHeaders.length).toBe(0)
45+
})
46+
47+
it('should not send set-cookie header when cookie value is accessed but not modified', async () => {
48+
const app = new Elysia()
49+
.get('/read', ({ cookie: { session } }) => {
50+
// Just reading the value
51+
const val = session.value
52+
return { read: val }
53+
})
54+
.get('/write', ({ cookie: { session } }) => {
55+
// Writing the value
56+
session.value = 'test'
57+
return { written: true }
58+
})
59+
60+
// Read endpoint should not set cookie
61+
const readResponse = await app.handle(
62+
new Request('http://localhost/read')
63+
)
64+
expect(readResponse.headers.getAll('set-cookie').length).toBe(0)
65+
66+
// Write endpoint should set cookie
67+
const writeResponse = await app.handle(
68+
new Request('http://localhost/write')
69+
)
70+
expect(
71+
writeResponse.headers.getAll('set-cookie').length
72+
).toBeGreaterThan(0)
73+
})
74+
75+
it('should not send set-cookie header when setting same value', async () => {
76+
const app = new Elysia().get('/same', ({ cookie: { session } }) => {
77+
// Setting the same value that came from request
78+
session.value = 'existing'
79+
return 'ok'
80+
})
81+
82+
const response = await app.handle(
83+
new Request('http://localhost/same', {
84+
headers: {
85+
cookie: 'session=existing'
86+
}
87+
})
88+
)
89+
90+
expect(response.headers.getAll('set-cookie').length).toBe(0)
91+
})
92+
93+
it('should send set-cookie header when value actually changes', async () => {
94+
const app = new Elysia().get('/change', ({ cookie: { session } }) => {
95+
session.value = 'new-value'
96+
return 'ok'
97+
})
98+
99+
const response = await app.handle(
100+
new Request('http://localhost/change', {
101+
headers: {
102+
cookie: 'session=old-value'
103+
}
104+
})
105+
)
106+
107+
expect(response.headers.getAll('set-cookie').length).toBeGreaterThan(0)
108+
})
109+
})

0 commit comments

Comments
 (0)