Skip to content

Commit 42c9b93

Browse files
feat: better Vary header support (#9993)
- support caching of responses with `Vary` header (possible without any changes on the client because since #8754 we're taking headers into account for the cache key) - fix browser caching of adjacent pages/endpoints fixes #9780 --------- Co-authored-by: S. Elliott Johnson <sejohnson@torchcloudconsulting.com>
1 parent f2c6e4b commit 42c9b93

File tree

7 files changed

+35
-11
lines changed

7 files changed

+35
-11
lines changed

.changeset/shy-ears-report.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@sveltejs/kit': minor
3+
---
4+
5+
feat: support caching of responses with `Vary` header (except for `Vary: *`)
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@sveltejs/kit': patch
3+
---
4+
5+
fix: include `Vary: Accept` header to fix browser caching of adjacent pages and endpoints

documentation/docs/20-core-concepts/10-routing.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -330,7 +330,8 @@ export async function POST({ request }) {
330330
`+server.js` files can be placed in the same directory as `+page` files, allowing the same route to be either a page or an API endpoint. To determine which, SvelteKit applies the following rules:
331331
332332
- `PUT`/`PATCH`/`DELETE`/`OPTIONS` requests are always handled by `+server.js` since they do not apply to pages
333-
- `GET`/`POST` requests are treated as page requests if the `accept` header prioritises `text/html` (in other words, it's a browser page request), else they are handled by `+server.js`
333+
- `GET`/`POST` requests are treated as page requests if the `accept` header prioritises `text/html` (in other words, it's a browser page request), else they are handled by `+server.js`.
334+
- Responses to `GET` requests will inlcude a `Vary: Accept` header, so that proxies and browsers cache HTML and JSON responses separately.
334335
335336
## $types
336337

packages/kit/src/runtime/server/page/serialize_data.js

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -46,16 +46,16 @@ export function serialize_data(fetched, filter, prerendering = false) {
4646

4747
let cache_control = null;
4848
let age = null;
49-
let vary = false;
49+
let varyAny = false;
5050

5151
for (const [key, value] of fetched.response.headers) {
5252
if (filter(key, value)) {
5353
headers[key] = value;
5454
}
5555

5656
if (key === 'cache-control') cache_control = value;
57-
if (key === 'age') age = value;
58-
if (key === 'vary') vary = true;
57+
else if (key === 'age') age = value;
58+
else if (key === 'vary' && value.trim() === '*') varyAny = true;
5959
}
6060

6161
const payload = {
@@ -89,10 +89,9 @@ export function serialize_data(fetched, filter, prerendering = false) {
8989
}
9090

9191
// Compute the time the response should be cached, taking into account max-age and age.
92-
// Do not cache at all if a vary header is present, as this indicates that the cache is
93-
// likely to get busted. It would also mean we'd have to add more logic to computing the
94-
// selector on the client which results in more code for 99% of people for the 1% who use vary.
95-
if (!prerendering && fetched.method === 'GET' && cache_control && !vary) {
92+
// Do not cache at all if a `Vary: *` header is present, as this indicates that the
93+
// cache is likely to get busted.
94+
if (!prerendering && fetched.method === 'GET' && cache_control && !varyAny) {
9695
const match = /s-maxage=(\d+)/g.exec(cache_control) ?? /max-age=(\d+)/g.exec(cache_control);
9796
if (match) {
9897
const ttl = +match[1] - +(age ?? '0');

packages/kit/src/runtime/server/page/serialize_data.spec.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ test('computes ttl using cache-control and age headers', () => {
8181
);
8282
});
8383

84-
test('doesnt compute ttl when vary header is present', () => {
84+
test('doesnt compute ttl when vary * header is present', () => {
8585
const raw = 'an "attr" & a \ud800';
8686
const escaped = 'an &quot;attr&quot; &amp; a &#55296;';
8787
const response_body = '';
@@ -93,7 +93,7 @@ test('doesnt compute ttl when vary header is present', () => {
9393
request_body: null,
9494
response_body,
9595
response: new Response(response_body, {
96-
headers: { 'cache-control': 'max-age=10', vary: 'accept-encoding' }
96+
headers: { 'cache-control': 'max-age=10', vary: '*' }
9797
})
9898
},
9999
() => false

packages/kit/src/runtime/server/respond.js

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -402,6 +402,18 @@ export async function respond(request, options, manifest, state) {
402402
throw new Error('This should never happen');
403403
}
404404

405+
// If the route contains a page and an endpoint, we need to add a
406+
// `Vary: Accept` header to the response because of browser caching
407+
if (request.method === 'GET' && route.page && route.endpoint) {
408+
const vary = response.headers
409+
.get('vary')
410+
?.split(',')
411+
?.map((v) => v.trim().toLowerCase());
412+
if (!(vary?.includes('accept') || vary?.includes('*'))) {
413+
response.headers.append('Vary', 'Accept');
414+
}
415+
}
416+
405417
return response;
406418
}
407419

packages/kit/test/apps/basics/test/client.test.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -650,7 +650,9 @@ test.describe('data-sveltekit attributes', () => {
650650

651651
test.describe('Content negotiation', () => {
652652
test('+server.js next to +page.svelte works', async ({ page }) => {
653-
await page.goto('/routing/content-negotiation');
653+
const response = await page.goto('/routing/content-negotiation');
654+
655+
expect(response.headers()['vary']).toBe('Accept');
654656
expect(await page.textContent('p')).toBe('Hi');
655657

656658
const pre = page.locator('pre');

0 commit comments

Comments
 (0)