Skip to content

Commit 644fb85

Browse files
fix(server): cache handler responses that also set cookies
i18n writes ubean_locale on every response; rejecting Set-Cookie made cachedEventHandler a no-op. Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent f9025e8 commit 644fb85

3 files changed

Lines changed: 40 additions & 4 deletions

File tree

examples/ubean-test/test/cache.test.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import {
99
resolveRouteCacheRules
1010
} from 'ubean';
1111
import type { UbeanContext } from 'ubean';
12-
import { getJson } from './helper';
12+
import { getJson, postJson } from './helper';
1313

1414
describe('Cache system', () => {
1515
describe('createMemoryStore() - memory storage', () => {
@@ -186,12 +186,13 @@ describe('Cache system', () => {
186186

187187
describe('HTTP integration - /api/cache-test (cachedEventHandler)', () => {
188188
it('returns cached result on second call', async () => {
189+
await postJson('/api/cache-test');
189190
const res1 = await getJson('/api/cache-test');
190191
const res2 = await getJson('/api/cache-test');
191192
expect(res1.status).toBe(200);
192193
expect(res2.status).toBe(200);
193-
// Both should return the same timestamp (cached)
194194
expect((res1.data as { timestamp: number }).timestamp).toBe((res2.data as { timestamp: number }).timestamp);
195+
expect(res2.headers.get('x-cache')).toBe('HIT');
195196
});
196197
});
197198
});

packages/server/src/cache.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -260,9 +260,10 @@ function isCacheableRequest(c: Context): boolean {
260260

261261
function isCacheableResponse(res: Response): boolean {
262262
if (res.status !== 200) return false;
263-
if (res.headers.get('set-cookie')) return false;
264263
const cc = res.headers.get('cache-control');
265264
if (cc && (cc.includes('private') || cc.includes('no-store'))) return false;
265+
// Set-Cookie (e.g. i18n `ubean_locale`) is stripped in serializeResponse.
266+
// Rejecting it here made `cachedEventHandler` a no-op in apps with i18n.
266267
return true;
267268
}
268269

@@ -333,11 +334,11 @@ export function cachedEventHandler<T extends (c: Context) => Response | Promise<
333334
handler: T,
334335
options: CacheRule = { ttl: 60 }
335336
): T {
336-
const store = useCacheStore();
337337
const ttl = options.ttl;
338338
const keyBase = options.name;
339339

340340
return (async (c: Context) => {
341+
const store = useCacheStore();
341342
const key = keyBase || buildCacheKey(c);
342343

343344
if (ttl > 0 && isCacheableRequest(c)) {
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import { afterEach, describe, expect, it } from 'vitest';
2+
import { Hono } from 'hono';
3+
import { cachedEventHandler, clearCacheStore, createMemoryStore, useCacheStore } from '../src/cache';
4+
5+
afterEach(() => {
6+
clearCacheStore();
7+
});
8+
9+
describe('cachedEventHandler', () => {
10+
it('caches JSON even when the live response sets a cookie', async () => {
11+
useCacheStore(createMemoryStore());
12+
let n = 0;
13+
const app = new Hono();
14+
app.get(
15+
'/x',
16+
cachedEventHandler(
17+
c => {
18+
n += 1;
19+
c.header('Set-Cookie', 'ubean_locale=en; Path=/; SameSite=Lax');
20+
return c.json({ n });
21+
},
22+
{ ttl: 60, name: 'cookie-ok' }
23+
)
24+
);
25+
26+
const miss = await app.request('/x');
27+
const hit = await app.request('/x');
28+
expect(miss.status).toBe(200);
29+
expect((await miss.json()) as { n: number }).toEqual({ n: 1 });
30+
expect((await hit.json()) as { n: number }).toEqual({ n: 1 });
31+
expect(hit.headers.get('x-cache')).toBe('HIT');
32+
expect(n).toBe(1);
33+
});
34+
});

0 commit comments

Comments
 (0)