Skip to content

Commit

Permalink
Implement way to alter which headers should be cached (#30)
Browse files Browse the repository at this point in the history
  • Loading branch information
dulnan committed Apr 7, 2024
1 parent a45c819 commit 057acca
Show file tree
Hide file tree
Showing 10 changed files with 163 additions and 21 deletions.
35 changes: 35 additions & 0 deletions docs/features/route-cache.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,41 @@ served from cache by the cached item `api_query_products_1`:
- /api/query/products?foobar=456&id=123
- /api/query/products?foobar=456&id=123&whatever=string&does=not&matter=at-all
### Alter which headers that are cached
You can define a method that receives the headers of the response and returns
the altered headers. The method is called right before the response is written
to cache.
::: warning
By default all headers are stored in the cache, because it is assumed that your
app already makes sure to not mark a response as cacheable during rendering. You
can however alter the headers that are stored in the cache. Keep in mind that
this might introduce side effects: If you use `useCookie()` to set a cookie and
then remove the `Set-Cookie` header using this approach, only the first request
will actually receive the `Set-Cookie` header. All subsequent requests that are
served from cache won't have this header.
:::
::: code-group
```typescript [multiCache.serverOptions.ts]
import { defineMultiCacheOptions } from 'nuxt-multi-cache/dist/runtime/serverOptions'

export default defineMultiCacheOptions({
route: {
alterCachedHeaders(headers) {
headers['set-cookie'] = undefined
return headers
},
},
})
```
:::
## Usage in Components
Use the `useRouteCache` composable in a page, layout or any component:
Expand Down
23 changes: 22 additions & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

21 changes: 21 additions & 0 deletions playground/app/multiCache.serverOptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,27 @@ export default defineMultiCacheOptions({
driver: customDriver(),
},
},
route: {
alterCachedHeaders(headers) {
const cookie = headers['set-cookie']
// Remove the SESSION cookie.
if (cookie) {
if (typeof cookie === 'string') {
if (cookie.includes('SESSION')) {
headers['set-cookie'] = undefined
}
} else if (Array.isArray(cookie)) {
const remaining = cookie.filter((v) => !v.includes('SESSION'))
if (!remaining.length) {
headers['set-cookie'] = undefined
} else {
headers['set-cookie'] = remaining
}
}
}
return headers
},
},
component: {},
cacheKeyPrefix: (event: H3Event): Promise<string> => {
return Promise.resolve(getCacheKeyPrefix(event))
Expand Down
17 changes: 17 additions & 0 deletions playground/pages/cachedPageWithCountryCookie.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<template>
<div id="data-cache-value">COUNTRY COOKIE: {{ country }}</div>
</template>

<script lang="ts" setup>
import { useRouteCache, useCookie } from '#imports'
const country = useCookie('country', {
default() {
return 'us'
},
})
useRouteCache((helper) => {
helper.setCacheable().setMaxAge(9000)
})
</script>
7 changes: 1 addition & 6 deletions playground/pages/cachedPageWithRandomNumber.vue
Original file line number Diff line number Diff line change
@@ -1,16 +1,11 @@
<template>
<div id="data-cache-value">RANDOM[{{ random }}]</div>
<div id="data-cache-value">COOKIE COUNTER[{{ counter }}]</div>
</template>

<script lang="ts" setup>
import { computed } from 'vue'
import { useAsyncData, useCookie } from 'nuxt/app'
import { useAsyncData } from 'nuxt/app'
import { useRouteCache } from '#imports'
const counter = useCookie('counter')
counter.value = counter.value || Math.round(Math.random() * 1000)
const { data: random } = await useAsyncData(() => {
return Promise.resolve(Math.round(Math.random() * 1000000000).toString())
})
Expand Down
17 changes: 17 additions & 0 deletions playground/pages/cachedPageWithSessionCookie.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<template>
<div id="data-cache-value">SESSION COOKIE: {{ session }}</div>
</template>

<script lang="ts" setup>
import { useRouteCache, useCookie } from '#imports'
const session = useCookie('SESSION', {
default() {
return Math.round(Math.random() * 1000).toString()
},
})
useRouteCache((helper) => {
helper.setCacheable().setMaxAge(9000)
})
</script>
7 changes: 5 additions & 2 deletions src/runtime/serverHandler/responseSend.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,8 +91,11 @@ export default defineEventHandler((event) => {
return _end.call(event.node.res, arg1, arg2, arg3)
}

const headers = response.getHeaders()
headers['set-cookie'] = undefined
let headers = { ...response.getHeaders() }

if (serverOptions.route?.alterCachedHeaders) {
headers = serverOptions.route.alterCachedHeaders(headers)
}

const cacheItem = encodeRouteCacheItem(
chunk,
Expand Down
11 changes: 11 additions & 0 deletions src/runtime/types.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { CreateStorageOptions, Storage } from 'unstorage'
import type { H3Event } from 'h3'
import { OutgoingHttpHeaders } from 'node:http'

interface CacheConfigOptions {
/**
Expand Down Expand Up @@ -187,6 +188,16 @@ export type MultiCacheServerOptions = {
* Provide a custom function that builds the cache key for a route.
*/
buildCacheKey?: (event: H3Event) => string

/**
* Alter the headers that are stored in the cache.
*
* You can use this to prevent certain headers from ever being cached,
* such as Set-Cookie.
*/
alterCachedHeaders?: (
headers: OutgoingHttpHeaders,
) => OutgoingHttpHeaders | Record<string, any>
}

/**
Expand Down
38 changes: 28 additions & 10 deletions test/routeCache.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { setup, $fetch } from '@nuxt/test-utils'
import { describe, expect, test } from 'vitest'
import { NuxtMultiCacheOptions } from '../src/runtime/types'
import purgeAll from './__helpers__/purgeAll'
import { decodeRouteCacheItem } from '../src/runtime/helpers/cacheItem'

describe('The route cache feature', async () => {
const multiCache: NuxtMultiCacheOptions = {
Expand Down Expand Up @@ -69,11 +70,30 @@ describe('The route cache feature', async () => {
expect(first).not.toEqual(second)
})

test('does not cache a pages cookie header', async () => {
test('does not cache the set-cookie header if it is a session cookie.', async () => {
await purgeAll()

// First call puts it into cache.
const first = await $fetch('/cachedPageWithRandomNumber', {
await $fetch('/cachedPageWithSessionCookie', {
method: 'get',
})

const cache = await $fetch(`/__nuxt_multi_cache/stats/route`, {
headers: {
'x-nuxt-multi-cache-token': 'hunter2',
},
})

const cacheItem = decodeRouteCacheItem(cache.rows[0].data)

expect(cacheItem?.headers['set-cookie']).toEqual(undefined)
})

test('does cache the set-cookie header if it is not a session cookie.', async () => {
await purgeAll()

// First call puts it into cache.
await $fetch('/cachedPageWithCountryCookie', {
method: 'get',
})

Expand All @@ -83,14 +103,12 @@ describe('The route cache feature', async () => {
},
})

const cacheItem = cache.rows[0].data.split('<CACHE_ITEM>')
let response = cacheItem[0]
try {
response = JSON.parse(cacheItem[0])
} catch (e) {
// ignore
}
const cacheItem = decodeRouteCacheItem(cache.rows[0].data)

expect(response.headers['set-cookie']).toEqual(undefined)
expect(cacheItem?.headers['set-cookie']).toMatchInlineSnapshot(`
[
"country=us; Path=/",
]
`)
})
})
8 changes: 6 additions & 2 deletions test/serverHandler/responseSend.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { describe, expect, test, vi, afterEach, beforeEach } from 'vitest'
import { describe, expect, test, vi } from 'vitest'
import { createStorage } from 'unstorage'
import { sleep } from '../__helpers__'
import responseSend from '../../src/runtime/serverHandler/responseSend'
Expand Down Expand Up @@ -124,6 +124,7 @@ describe('responseSend server handler', () => {
path: '/test/route/nested',
node: {
res: {
statusCode: 200,
end: () => {},
getHeaders() {
return {
Expand All @@ -149,7 +150,7 @@ describe('responseSend server handler', () => {
await sleep(100)

expect(await storage.getItemRaw('test:route:nested')).toMatchInlineSnapshot(
'"{\\"headers\\":{\\"x-test\\":\\"Foobar\\"},\\"cacheTags\\":[]}<CACHE_ITEM><html></html>"',
'"{\\"headers\\":{\\"x-test\\":\\"Foobar\\"},\\"statusCode\\":200,\\"cacheTags\\":[]}<CACHE_ITEM><html></html>"',
)
})

Expand All @@ -159,6 +160,7 @@ describe('responseSend server handler', () => {
path: '/test/route/nested',
node: {
res: {
statusCode: 200,
end: () => {},
getHeaders() {
return {
Expand Down Expand Up @@ -192,6 +194,7 @@ describe('responseSend server handler', () => {
path: '/test/route/nested',
node: {
res: {
statusCode: 200,
end: () => {},
getHeaders() {
return {
Expand Down Expand Up @@ -225,6 +228,7 @@ describe('responseSend server handler', () => {
path: '/test/route/nested',
node: {
res: {
statusCode: 200,
end: () => {},
getHeaders() {
return {
Expand Down

0 comments on commit 057acca

Please sign in to comment.