From 24ae336f7e5d914d6f0cd159c2f0b195a4151b69 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sun, 4 Sep 2022 15:23:34 -0400 Subject: [PATCH 1/7] refactor --- packages/kit/src/runtime/server/page/fetch.js | 10 ++--- .../src/runtime/server/page/serialize_data.js | 29 +++++++++---- .../server/page/serialize_data.spec.js | 41 ++++++++----------- .../kit/src/runtime/server/page/types.d.ts | 12 ++---- 4 files changed, 45 insertions(+), 47 deletions(-) diff --git a/packages/kit/src/runtime/server/page/fetch.js b/packages/kit/src/runtime/server/page/fetch.js index 703179fe3d0e..d8383e5b8b93 100644 --- a/packages/kit/src/runtime/server/page/fetch.js +++ b/packages/kit/src/runtime/server/page/fetch.js @@ -233,13 +233,9 @@ export function create_fetch({ event, options, state, route, prerender_default } fetched.push({ url: requested, method: opts.method || 'GET', - body: opts.body, - response: { - status: status_number, - statusText: response.statusText, - headers, - body - } + request_body: opts.body, + response_body: body, + response: response }); } diff --git a/packages/kit/src/runtime/server/page/serialize_data.js b/packages/kit/src/runtime/server/page/serialize_data.js index 24a86a260b53..74e2c158cbab 100644 --- a/packages/kit/src/runtime/server/page/serialize_data.js +++ b/packages/kit/src/runtime/server/page/serialize_data.js @@ -40,10 +40,23 @@ const pattern = new RegExp(`[${Object.keys(replacements).join('')}]`, 'g'); * @example const html = serialize_data('/data.json', null, { foo: 'bar' }); */ export function serialize_data(fetched, prerendering = false) { - const safe_payload = JSON.stringify(fetched.response).replace( - pattern, - (match) => replacements[match] - ); + /** @type {Record} */ + const headers = {}; + + for (const [key, value] of fetched.response.headers) { + const lower = key.toLowerCase(); + if (lower === 'set-cookie' || lower === 'etag') continue; + headers[key] = value; + } + + const payload = { + status: fetched.response.status, + statusText: fetched.response.statusText, + headers, + body: fetched.response_body + }; + + const safe_payload = JSON.stringify(payload).replace(pattern, (match) => replacements[match]); const attrs = [ 'type="application/json"', @@ -51,16 +64,16 @@ export function serialize_data(fetched, prerendering = false) { `data-url=${escape_html_attr(fetched.url)}` ]; - if (fetched.body) { - attrs.push(`data-hash=${escape_html_attr(hash(fetched.body))}`); + if (fetched.request_body) { + attrs.push(`data-hash=${escape_html_attr(hash(fetched.request_body))}`); } if (!prerendering && fetched.method === 'GET') { - const cache_control = /** @type {string} */ (fetched.response.headers['cache-control']); + const cache_control = fetched.response.headers.get('cache-control'); if (cache_control) { const match = /s-maxage=(\d+)/g.exec(cache_control) ?? /max-age=(\d+)/g.exec(cache_control); if (match) { - const age = /** @type {string} */ (fetched.response.headers['age']) ?? '0'; + const age = fetched.response.headers.get('age') ?? '0'; const ttl = +match[1] - +age; attrs.push(`data-ttl="${ttl}"`); diff --git a/packages/kit/src/runtime/server/page/serialize_data.spec.js b/packages/kit/src/runtime/server/page/serialize_data.spec.js index 3383c7f98bb2..a180bdc12ba6 100644 --- a/packages/kit/src/runtime/server/page/serialize_data.spec.js +++ b/packages/kit/src/runtime/server/page/serialize_data.spec.js @@ -3,39 +3,35 @@ import * as assert from 'uvu/assert'; import { serialize_data } from './serialize_data.js'; test('escapes slashes', () => { + const response_body = '' ); }); test('escapes exclamation marks', () => { + const response_body = 'alert("xss")'; + assert.equal( serialize_data({ url: 'foo', method: 'GET', - body: null, - response: { - status: 200, - statusText: 'OK', - headers: {}, - body: 'alert("xss")' - } + request_body: null, + response_body, + response: new Response(response_body) }), '' ); }); @@ -43,19 +39,16 @@ test('escapes exclamation marks', () => { test('escapes the attribute values', () => { const raw = 'an "attr" & a \ud800'; const escaped = 'an "attr" & a �'; + const response_body = ''; assert.equal( serialize_data({ url: raw, method: 'GET', - body: null, - response: { - status: 200, - statusText: 'OK', - headers: {}, - body: '' - } + request_body: null, + response_body, + response: new Response(response_body) }), - `` + `` ); }); diff --git a/packages/kit/src/runtime/server/page/types.d.ts b/packages/kit/src/runtime/server/page/types.d.ts index 4da2b3d18171..648baaf7bf05 100644 --- a/packages/kit/src/runtime/server/page/types.d.ts +++ b/packages/kit/src/runtime/server/page/types.d.ts @@ -1,16 +1,12 @@ -import { ResponseHeaders, SSRNode, CspDirectives } from 'types'; +import { SSRNode, CspDirectives } from 'types'; import { HttpError } from '../../control.js'; export interface Fetched { url: string; method: string; - body?: string | null; - response: { - status: number; - statusText: string; - headers: ResponseHeaders; - body: string; - }; + request_body?: string | null; + response_body: string; + response: Response; } export interface FetchState { From ad0d1bd5c1b3e89ccf455dac0a4822354f691307 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sun, 4 Sep 2022 16:12:27 -0400 Subject: [PATCH 2/7] add filterSerializedResponseHeaders option - closes #1971 --- .changeset/silent-cycles-reply.md | 5 ++ documentation/docs/05-load.md | 2 +- documentation/docs/06-hooks.md | 4 +- packages/kit/src/runtime/server/index.js | 8 ++- .../kit/src/runtime/server/page/render.js | 6 +- .../src/runtime/server/page/serialize_data.js | 8 ++- .../server/page/serialize_data.spec.js | 57 +++++++++++-------- packages/kit/test/apps/basics/test/test.js | 10 +--- packages/kit/types/index.d.ts | 1 + 9 files changed, 62 insertions(+), 39 deletions(-) create mode 100644 .changeset/silent-cycles-reply.md diff --git a/.changeset/silent-cycles-reply.md b/.changeset/silent-cycles-reply.md new file mode 100644 index 000000000000..394f8b56f3da --- /dev/null +++ b/.changeset/silent-cycles-reply.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': patch +--- + +[breaking] exclude headers from serialized responses by default, add filterSerializedResponseHeaders resolve option diff --git a/documentation/docs/05-load.md b/documentation/docs/05-load.md index 0aaf5389dffc..6da83a4c25f2 100644 --- a/documentation/docs/05-load.md +++ b/documentation/docs/05-load.md @@ -143,7 +143,7 @@ export async function load({ depends }) { - it can be used to make credentialed requests on the server, as it inherits the `cookie` and `authorization` headers for the page request - it can make relative requests on the server (ordinarily, `fetch` requires a URL with an origin when used in a server context) - internal requests (e.g. for `+server.js` routes) go direct to the handler function when running on the server, without the overhead of an HTTP call -- during server-side rendering, the response will be captured and inlined into the rendered HTML +- during server-side rendering, the response will be captured and inlined into the rendered HTML. Note that headers will _not_ be serialized, unless explicitly included via [`filterSerializedResponseHeaders`](/docs/hooks#handle) - during hydration, the response will be read from the HTML, guaranteeing consistency and preventing an additional network request > Cookies will only be passed through if the target host is the same as the SvelteKit application or a more specific subdomain of it. diff --git a/documentation/docs/06-hooks.md b/documentation/docs/06-hooks.md index 7bf25b938010..add2cc392bc6 100644 --- a/documentation/docs/06-hooks.md +++ b/documentation/docs/06-hooks.md @@ -64,13 +64,15 @@ You can add call multiple `handle` functions with [the `sequence` helper functio `resolve` also supports a second, optional parameter that gives you more control over how the response will be rendered. That parameter is an object that can have the following fields: - `transformPageChunk(opts: { html: string, done: boolean }): MaybePromise` — applies custom transforms to HTML. If `done` is true, it's the final chunk. Chunks are not guaranteed to be well-formed HTML (they could include an element's opening tag but not its closing tag, for example) but they will always be split at sensible boundaries such as `%sveltekit.head%` or layout/page components. +- `filterSerializedResponseHeaders(name: string, value: string): boolean` — determines which headers should be included in serialized responses when a `load` function loads a resource with `fetch`. By default, none will be included ```js /// file: src/hooks.js /** @type {import('@sveltejs/kit').Handle} */ export async function handle({ event, resolve }) { const response = await resolve(event, { - transformPageChunk: ({ html }) => html.replace('old', 'new') + transformPageChunk: ({ html }) => html.replace('old', 'new'), + filterSerializedResponseHeaders: (name) => name.startsWith('x-') }); return response; diff --git a/packages/kit/src/runtime/server/index.js b/packages/kit/src/runtime/server/index.js index ec142c6ece25..45acb2e474ca 100644 --- a/packages/kit/src/runtime/server/index.js +++ b/packages/kit/src/runtime/server/index.js @@ -14,6 +14,8 @@ import { DATA_SUFFIX } from '../../constants.js'; /** @param {{ html: string }} opts */ const default_transform = ({ html }) => html; +const default_filter = () => false; + /** @type {import('types').Respond} */ export async function respond(request, options, state) { let url = new URL(request.url); @@ -201,7 +203,8 @@ export async function respond(request, options, state) { /** @type {import('types').RequiredResolveOptions} */ let resolve_opts = { - transformPageChunk: default_transform + transformPageChunk: default_transform, + filterSerializedResponseHeaders: default_filter }; /** @@ -226,7 +229,8 @@ export async function respond(request, options, state) { } resolve_opts = { - transformPageChunk: opts.transformPageChunk || default_transform + transformPageChunk: opts.transformPageChunk || default_transform, + filterSerializedResponseHeaders: opts.filterSerializedResponseHeaders || default_filter }; } diff --git a/packages/kit/src/runtime/server/page/render.js b/packages/kit/src/runtime/server/page/render.js index ec03e07122dd..7539dccaa4d5 100644 --- a/packages/kit/src/runtime/server/page/render.js +++ b/packages/kit/src/runtime/server/page/render.js @@ -284,7 +284,11 @@ export async function render_response({ } if (page_config.ssr && page_config.csr) { - body += `\n\t${fetched.map((item) => serialize_data(item, !!state.prerendering)).join('\n\t')}`; + body += `\n\t${fetched + .map((item) => + serialize_data(item, resolve_opts.filterSerializedResponseHeaders, !!state.prerendering) + ) + .join('\n\t')}`; } if (options.service_worker) { diff --git a/packages/kit/src/runtime/server/page/serialize_data.js b/packages/kit/src/runtime/server/page/serialize_data.js index 74e2c158cbab..2d2e9e4d7bbe 100644 --- a/packages/kit/src/runtime/server/page/serialize_data.js +++ b/packages/kit/src/runtime/server/page/serialize_data.js @@ -35,18 +35,20 @@ const pattern = new RegExp(`[${Object.keys(replacements).join('')}]`, 'g'); * and that the resulting string isn't further modified. * * @param {import('./types.js').Fetched} fetched + * @param {(name: string, value: string) => boolean} filter * @param {boolean} [prerendering] * @returns {string} The raw HTML of a script element carrying the JSON payload. * @example const html = serialize_data('/data.json', null, { foo: 'bar' }); */ -export function serialize_data(fetched, prerendering = false) { +export function serialize_data(fetched, filter, prerendering = false) { /** @type {Record} */ const headers = {}; for (const [key, value] of fetched.response.headers) { const lower = key.toLowerCase(); - if (lower === 'set-cookie' || lower === 'etag') continue; - headers[key] = value; + if (filter(lower, value)) { + headers[lower] = value; + } } const payload = { diff --git a/packages/kit/src/runtime/server/page/serialize_data.spec.js b/packages/kit/src/runtime/server/page/serialize_data.spec.js index a180bdc12ba6..57ef9ee14d32 100644 --- a/packages/kit/src/runtime/server/page/serialize_data.spec.js +++ b/packages/kit/src/runtime/server/page/serialize_data.spec.js @@ -6,15 +6,18 @@ test('escapes slashes', () => { const response_body = '' ); }); @@ -23,15 +26,18 @@ test('escapes exclamation marks', () => { const response_body = 'alert("xss")'; assert.equal( - serialize_data({ - url: 'foo', - method: 'GET', - request_body: null, - response_body, - response: new Response(response_body) - }), + serialize_data( + { + url: 'foo', + method: 'GET', + request_body: null, + response_body, + response: new Response(response_body) + }, + () => false + ), '' ); }); @@ -41,14 +47,17 @@ test('escapes the attribute values', () => { const escaped = 'an "attr" & a �'; const response_body = ''; assert.equal( - serialize_data({ - url: raw, - method: 'GET', - request_body: null, - response_body, - response: new Response(response_body) - }), - `` + serialize_data( + { + url: raw, + method: 'GET', + request_body: null, + response_body, + response: new Response(response_body) + }, + () => false + ), + `` ); }); diff --git a/packages/kit/test/apps/basics/test/test.js b/packages/kit/test/apps/basics/test/test.js index d46e73f175ce..82e741c3654e 100644 --- a/packages/kit/test/apps/basics/test/test.js +++ b/packages/kit/test/apps/basics/test/test.js @@ -665,8 +665,7 @@ test.describe('Load', () => { // by the time JS has run, hydration will have nuked these scripts const script_contents = await page.innerHTML('script[data-sveltekit-fetched]'); - const payload = - '{"status":200,"statusText":"","headers":{"content-type":"application/json"},"body":"{\\"answer\\":42}"}'; + const payload = '{"status":200,"statusText":"","headers":{},"body":"{\\"answer\\":42}"}'; expect(script_contents).toBe(payload); } @@ -684,11 +683,8 @@ test.describe('Load', () => { expect(await page.textContent('h1')).toBe('a: X'); expect(await page.textContent('h2')).toBe('b: Y'); - const payload_a = - '{"status":200,"statusText":"","headers":{"content-type":"text/plain;charset=UTF-8"},"body":"X"}'; - - const payload_b = - '{"status":200,"statusText":"","headers":{"content-type":"text/plain;charset=UTF-8"},"body":"Y"}'; + const payload_a = '{"status":200,"statusText":"","headers":{},"body":"X"}'; + const payload_b = '{"status":200,"statusText":"","headers":{},"body":"Y"}'; if (!javaScriptEnabled) { // by the time JS has run, hydration will have nuked these scripts diff --git a/packages/kit/types/index.d.ts b/packages/kit/types/index.d.ts index c09238532ea0..0b2e8636212e 100644 --- a/packages/kit/types/index.d.ts +++ b/packages/kit/types/index.d.ts @@ -273,6 +273,7 @@ export interface RequestHandler< export interface ResolveOptions { transformPageChunk?: (input: { html: string; done: boolean }) => MaybePromise; + filterSerializedResponseHeaders?: (name: string, value: string) => boolean; } export class Server { From 92c744b3d7e9d84fb9e8061a381bdd6097836f2d Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Mon, 5 Sep 2022 11:03:39 -0400 Subject: [PATCH 3/7] Update .changeset/silent-cycles-reply.md Co-authored-by: Conduitry --- .changeset/silent-cycles-reply.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/silent-cycles-reply.md b/.changeset/silent-cycles-reply.md index 394f8b56f3da..66f12e14d322 100644 --- a/.changeset/silent-cycles-reply.md +++ b/.changeset/silent-cycles-reply.md @@ -2,4 +2,4 @@ '@sveltejs/kit': patch --- -[breaking] exclude headers from serialized responses by default, add filterSerializedResponseHeaders resolve option +[breaking] exclude headers from serialized responses by default, add `filterSerializedResponseHeaders` `resolve` option From 54fae62db06949641bcee7758a9921b3794fc30e Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Mon, 5 Sep 2022 11:03:45 -0400 Subject: [PATCH 4/7] Update documentation/docs/06-hooks.md Co-authored-by: Conduitry --- documentation/docs/06-hooks.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/documentation/docs/06-hooks.md b/documentation/docs/06-hooks.md index 2fb32dc58245..8992b92ae266 100644 --- a/documentation/docs/06-hooks.md +++ b/documentation/docs/06-hooks.md @@ -64,7 +64,7 @@ You can add call multiple `handle` functions with [the `sequence` helper functio `resolve` also supports a second, optional parameter that gives you more control over how the response will be rendered. That parameter is an object that can have the following fields: - `transformPageChunk(opts: { html: string, done: boolean }): MaybePromise` — applies custom transforms to HTML. If `done` is true, it's the final chunk. Chunks are not guaranteed to be well-formed HTML (they could include an element's opening tag but not its closing tag, for example) but they will always be split at sensible boundaries such as `%sveltekit.head%` or layout/page components. -- `filterSerializedResponseHeaders(name: string, value: string): boolean` — determines which headers should be included in serialized responses when a `load` function loads a resource with `fetch`. By default, none will be included +- `filterSerializedResponseHeaders(name: string, value: string): boolean` — determines which headers should be included in serialized responses when a `load` function loads a resource with `fetch`. By default, none will be included. ```js /// file: src/hooks.js From 7ecf4b959e7eb2c1a322488be47d62c6a572a100 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Mon, 5 Sep 2022 12:23:32 -0400 Subject: [PATCH 5/7] throw error if excluded header is read during SSR --- packages/kit/src/core/prerender/prerender.js | 18 +++++++---- packages/kit/src/runtime/server/page/fetch.js | 30 ++++++++++++------- packages/kit/src/runtime/server/page/index.js | 3 +- .../kit/src/runtime/server/page/render.js | 1 + .../runtime/server/page/respond_with_error.js | 3 +- .../src/runtime/server/page/serialize_data.js | 4 +-- ...vite.config.js.timestamp-1662392748524.mjs | 20 +++++++++++++ .../apps/basics/src/routes/load/+page.svelte | 2 +- .../+server.js | 2 +- .../+page.js | 2 +- .../+page.svelte | 0 .../fetch-response-headers.json/+server.js | 8 +++++ .../load/fetch-response-headers/+page.js | 9 ++++++ .../load/fetch-response-headers/+page.svelte | 7 +++++ packages/kit/test/apps/basics/test/test.js | 2 +- 15 files changed, 86 insertions(+), 25 deletions(-) create mode 100644 packages/kit/test/apps/amp/vite.config.js.timestamp-1662392748524.mjs rename packages/kit/test/apps/basics/src/routes/load/{fetch-headers.json => fetch-request-headers.json}/+server.js (80%) rename packages/kit/test/apps/basics/src/routes/load/{fetch-headers => fetch-request-headers}/+page.js (67%) rename packages/kit/test/apps/basics/src/routes/load/{fetch-headers => fetch-request-headers}/+page.svelte (100%) create mode 100644 packages/kit/test/apps/basics/src/routes/load/fetch-response-headers.json/+server.js create mode 100644 packages/kit/test/apps/basics/src/routes/load/fetch-response-headers/+page.js create mode 100644 packages/kit/test/apps/basics/src/routes/load/fetch-response-headers/+page.svelte diff --git a/packages/kit/src/core/prerender/prerender.js b/packages/kit/src/core/prerender/prerender.js index 52bd4d7bdc32..f5e4d981c8fc 100644 --- a/packages/kit/src/core/prerender/prerender.js +++ b/packages/kit/src/core/prerender/prerender.js @@ -237,10 +237,11 @@ export async function prerender() { const encoded_dependency_path = new URL(dependency_path, 'http://localhost').pathname; const decoded_dependency_path = decodeURI(encoded_dependency_path); - const prerender = result.response.headers.get('x-sveltekit-prerender'); + const headers = Object.fromEntries(result.response.headers); + const prerender = headers['x-sveltekit-prerender']; if (prerender) { - const route_id = /** @type {string} */ (result.response.headers.get('x-sveltekit-routeid')); + const route_id = headers['x-sveltekit-routeid']; const existing_value = prerender_map.get(route_id); if (existing_value !== 'auto') { prerender_map.set(route_id, prerender === 'true' ? true : 'auto'); @@ -259,7 +260,10 @@ export async function prerender() { ); } - if (config.prerender.crawl && response.headers.get('content-type') === 'text/html') { + // avoid triggering `filterSerializeResponseHeaders` guard + const headers = Object.fromEntries(response.headers); + + if (config.prerender.crawl && headers['content-type'] === 'text/html') { for (const href of crawl(body.toString())) { if (href.startsWith('data:') || href.startsWith('#')) continue; @@ -288,7 +292,9 @@ export async function prerender() { */ function save(category, response, body, decoded, encoded, referrer, referenceType) { const response_type = Math.floor(response.status / 100); - const type = /** @type {string} */ (response.headers.get('content-type')); + const headers = Object.fromEntries(response.headers); + + const type = headers['content-type']; const is_html = response_type === REDIRECT || type === 'text/html'; const file = output_filename(decoded, is_html); @@ -297,7 +303,7 @@ export async function prerender() { if (written.has(file)) return; if (response_type === REDIRECT) { - const location = response.headers.get('location'); + const location = headers['location']; if (location) { const resolved = resolve(encoded, location); @@ -305,7 +311,7 @@ export async function prerender() { enqueue(decoded, decodeURI(resolved), resolved); } - if (!response.headers.get('x-sveltekit-normalize')) { + if (!headers['x-sveltekit-normalize']) { mkdirp(dirname(dest)); log.warn(`${response.status} ${decoded} -> ${location}`); diff --git a/packages/kit/src/runtime/server/page/fetch.js b/packages/kit/src/runtime/server/page/fetch.js index 2697688ee703..525d6f755e2e 100644 --- a/packages/kit/src/runtime/server/page/fetch.js +++ b/packages/kit/src/runtime/server/page/fetch.js @@ -10,9 +10,10 @@ import { domain_matches, path_matches } from './cookie.js'; * state: import('types').SSRState; * route: import('types').SSRRoute | import('types').SSRErrorPage; * prerender_default?: import('types').PrerenderOption; + * resolve_opts: import('types').RequiredResolveOptions; * }} opts */ -export function create_fetch({ event, options, state, route, prerender_default }) { +export function create_fetch({ event, options, state, route, prerender_default, resolve_opts }) { /** @type {import('./types').Fetched[]} */ const fetched = []; @@ -189,16 +190,6 @@ export function create_fetch({ event, options, state, route, prerender_default } async function text() { const body = await response.text(); - // TODO just pass `response.headers`, for processing inside `serialize_data` - /** @type {import('types').ResponseHeaders} */ - const headers = {}; - for (const [key, value] of response.headers) { - // TODO skip others besides set-cookie and etag? - if (key !== 'set-cookie' && key !== 'etag') { - headers[key] = value; - } - } - if (!body || typeof body === 'string') { const status_number = Number(response.status); if (isNaN(status_number)) { @@ -218,6 +209,23 @@ export function create_fetch({ event, options, state, route, prerender_default } response_body: body, response: response }); + + // ensure that excluded headers can't be read + const get = response.headers.get; + response.headers.get = (key) => { + const lower = key.toLowerCase(); + const value = get.call(response.headers, lower); + if (value && !lower.startsWith('x-sveltekit-')) { + const included = resolve_opts.filterSerializedResponseHeaders(lower, value); + if (!included) { + throw new Error( + `Failed to get response header "${lower}" — it must be included by the \`filterSerializedResponseHeaders\` option: https://kit.svelte.dev/docs/hooks#handle` + ); + } + } + + return value; + }; } if (dependency) { diff --git a/packages/kit/src/runtime/server/page/index.js b/packages/kit/src/runtime/server/page/index.js index c704cdc83d15..d2b9c5b5858e 100644 --- a/packages/kit/src/runtime/server/page/index.js +++ b/packages/kit/src/runtime/server/page/index.js @@ -131,7 +131,8 @@ export async function render_page(event, route, page, options, state, resolve_op options, state, route, - prerender_default: should_prerender + prerender_default: should_prerender, + resolve_opts }); if (get_option(nodes, 'ssr') === false) { diff --git a/packages/kit/src/runtime/server/page/render.js b/packages/kit/src/runtime/server/page/render.js index 7539dccaa4d5..01beef8914a5 100644 --- a/packages/kit/src/runtime/server/page/render.js +++ b/packages/kit/src/runtime/server/page/render.js @@ -325,6 +325,7 @@ export async function render_response({ })) || ''; const headers = new Headers({ + 'x-sveltekit-page': 'true', 'content-type': 'text/html', etag: `"${hash(html)}"` }); diff --git a/packages/kit/src/runtime/server/page/respond_with_error.js b/packages/kit/src/runtime/server/page/respond_with_error.js index 739bc06b3674..b7a3964ad79d 100644 --- a/packages/kit/src/runtime/server/page/respond_with_error.js +++ b/packages/kit/src/runtime/server/page/respond_with_error.js @@ -25,7 +25,8 @@ export async function respond_with_error({ event, options, state, status, error, event, options, state, - route: GENERIC_ERROR + route: GENERIC_ERROR, + resolve_opts }); try { diff --git a/packages/kit/src/runtime/server/page/serialize_data.js b/packages/kit/src/runtime/server/page/serialize_data.js index 2d2e9e4d7bbe..7648eea180b2 100644 --- a/packages/kit/src/runtime/server/page/serialize_data.js +++ b/packages/kit/src/runtime/server/page/serialize_data.js @@ -71,11 +71,11 @@ export function serialize_data(fetched, filter, prerendering = false) { } if (!prerendering && fetched.method === 'GET') { - const cache_control = fetched.response.headers.get('cache-control'); + const cache_control = headers['cache-control']; if (cache_control) { const match = /s-maxage=(\d+)/g.exec(cache_control) ?? /max-age=(\d+)/g.exec(cache_control); if (match) { - const age = fetched.response.headers.get('age') ?? '0'; + const age = headers['age'] ?? '0'; const ttl = +match[1] - +age; attrs.push(`data-ttl="${ttl}"`); diff --git a/packages/kit/test/apps/amp/vite.config.js.timestamp-1662392748524.mjs b/packages/kit/test/apps/amp/vite.config.js.timestamp-1662392748524.mjs new file mode 100644 index 000000000000..2eae59527dcd --- /dev/null +++ b/packages/kit/test/apps/amp/vite.config.js.timestamp-1662392748524.mjs @@ -0,0 +1,20 @@ +// vite.config.js +import * as path from "path"; +import { sveltekit } from "@sveltejs/kit/vite"; +var config = { + build: { + minify: false + }, + clearScreen: false, + plugins: [sveltekit()], + server: { + fs: { + allow: [path.resolve("../../../src")] + } + } +}; +var vite_config_default = config; +export { + vite_config_default as default +}; +//# sourceMappingURL=data:application/json;base64,ewogICJ2ZXJzaW9uIjogMywKICAic291cmNlcyI6IFsidml0ZS5jb25maWcuanMiXSwKICAic291cmNlc0NvbnRlbnQiOiBbImNvbnN0IF9fdml0ZV9pbmplY3RlZF9vcmlnaW5hbF9kaXJuYW1lID0gXCIvVXNlcnMvcmljaC9EZXZlbG9wbWVudC9TVkVMVEUvS0lUL2tpdC9wYWNrYWdlcy9raXQvdGVzdC9hcHBzL2FtcFwiO2NvbnN0IF9fdml0ZV9pbmplY3RlZF9vcmlnaW5hbF9maWxlbmFtZSA9IFwiL1VzZXJzL3JpY2gvRGV2ZWxvcG1lbnQvU1ZFTFRFL0tJVC9raXQvcGFja2FnZXMva2l0L3Rlc3QvYXBwcy9hbXAvdml0ZS5jb25maWcuanNcIjtjb25zdCBfX3ZpdGVfaW5qZWN0ZWRfb3JpZ2luYWxfaW1wb3J0X21ldGFfdXJsID0gXCJmaWxlOi8vL1VzZXJzL3JpY2gvRGV2ZWxvcG1lbnQvU1ZFTFRFL0tJVC9raXQvcGFja2FnZXMva2l0L3Rlc3QvYXBwcy9hbXAvdml0ZS5jb25maWcuanNcIjtpbXBvcnQgKiBhcyBwYXRoIGZyb20gJ3BhdGgnO1xuaW1wb3J0IHsgc3ZlbHRla2l0IH0gZnJvbSAnQHN2ZWx0ZWpzL2tpdC92aXRlJztcblxuLyoqIEB0eXBlIHtpbXBvcnQoJ3ZpdGUnKS5Vc2VyQ29uZmlnfSAqL1xuY29uc3QgY29uZmlnID0ge1xuXHRidWlsZDoge1xuXHRcdG1pbmlmeTogZmFsc2Vcblx0fSxcblx0Y2xlYXJTY3JlZW46IGZhbHNlLFxuXHRwbHVnaW5zOiBbc3ZlbHRla2l0KCldLFxuXHRzZXJ2ZXI6IHtcblx0XHRmczoge1xuXHRcdFx0YWxsb3c6IFtwYXRoLnJlc29sdmUoJy4uLy4uLy4uL3NyYycpXVxuXHRcdH1cblx0fVxufTtcblxuZXhwb3J0IGRlZmF1bHQgY29uZmlnO1xuIl0sCiAgIm1hcHBpbmdzIjogIjtBQUFxWCxZQUFZLFVBQVU7QUFDM1ksU0FBUyxpQkFBaUI7QUFHMUIsSUFBTSxTQUFTO0FBQUEsRUFDZCxPQUFPO0FBQUEsSUFDTixRQUFRO0FBQUEsRUFDVDtBQUFBLEVBQ0EsYUFBYTtBQUFBLEVBQ2IsU0FBUyxDQUFDLFVBQVUsQ0FBQztBQUFBLEVBQ3JCLFFBQVE7QUFBQSxJQUNQLElBQUk7QUFBQSxNQUNILE9BQU8sQ0FBTSxhQUFRLGNBQWMsQ0FBQztBQUFBLElBQ3JDO0FBQUEsRUFDRDtBQUNEO0FBRUEsSUFBTyxzQkFBUTsiLAogICJuYW1lcyI6IFtdCn0K diff --git a/packages/kit/test/apps/basics/src/routes/load/+page.svelte b/packages/kit/test/apps/basics/src/routes/load/+page.svelte index 1685c738ef3e..9c46790b79c9 100644 --- a/packages/kit/test/apps/basics/src/routes/load/+page.svelte +++ b/packages/kit/test/apps/basics/src/routes/load/+page.svelte @@ -8,7 +8,7 @@ fetch request fetch relative fetch credentialed -fetch headers +fetch headers large response raw body server fetch request diff --git a/packages/kit/test/apps/basics/src/routes/load/fetch-headers.json/+server.js b/packages/kit/test/apps/basics/src/routes/load/fetch-request-headers.json/+server.js similarity index 80% rename from packages/kit/test/apps/basics/src/routes/load/fetch-headers.json/+server.js rename to packages/kit/test/apps/basics/src/routes/load/fetch-request-headers.json/+server.js index 28608043de70..d58d91263242 100644 --- a/packages/kit/test/apps/basics/src/routes/load/fetch-headers.json/+server.js +++ b/packages/kit/test/apps/basics/src/routes/load/fetch-request-headers.json/+server.js @@ -1,6 +1,6 @@ import { json } from '@sveltejs/kit'; -/** @type {import('@sveltejs/kit').RequestHandler} */ +/** @type {import('./$types').RequestHandler} */ export function GET({ request }) { /** @type {Record} */ const body = {}; diff --git a/packages/kit/test/apps/basics/src/routes/load/fetch-headers/+page.js b/packages/kit/test/apps/basics/src/routes/load/fetch-request-headers/+page.js similarity index 67% rename from packages/kit/test/apps/basics/src/routes/load/fetch-headers/+page.js rename to packages/kit/test/apps/basics/src/routes/load/fetch-request-headers/+page.js index 65f31059ef45..69e85a35b484 100644 --- a/packages/kit/test/apps/basics/src/routes/load/fetch-headers/+page.js +++ b/packages/kit/test/apps/basics/src/routes/load/fetch-request-headers/+page.js @@ -1,6 +1,6 @@ /** @type {import('@sveltejs/kit').Load} */ export async function load({ fetch }) { - const res = await fetch('/load/fetch-headers.json'); + const res = await fetch('/load/fetch-request-headers.json'); return { headers: await res.json() diff --git a/packages/kit/test/apps/basics/src/routes/load/fetch-headers/+page.svelte b/packages/kit/test/apps/basics/src/routes/load/fetch-request-headers/+page.svelte similarity index 100% rename from packages/kit/test/apps/basics/src/routes/load/fetch-headers/+page.svelte rename to packages/kit/test/apps/basics/src/routes/load/fetch-request-headers/+page.svelte diff --git a/packages/kit/test/apps/basics/src/routes/load/fetch-response-headers.json/+server.js b/packages/kit/test/apps/basics/src/routes/load/fetch-response-headers.json/+server.js new file mode 100644 index 000000000000..f85e58c595a9 --- /dev/null +++ b/packages/kit/test/apps/basics/src/routes/load/fetch-response-headers.json/+server.js @@ -0,0 +1,8 @@ +/** @type {import('./$types').RequestHandler} */ +export function GET() { + return new Response('ok', { + headers: { + 'x-foo': 'this should not appear' + } + }); +} diff --git a/packages/kit/test/apps/basics/src/routes/load/fetch-response-headers/+page.js b/packages/kit/test/apps/basics/src/routes/load/fetch-response-headers/+page.js new file mode 100644 index 000000000000..4ca808378220 --- /dev/null +++ b/packages/kit/test/apps/basics/src/routes/load/fetch-response-headers/+page.js @@ -0,0 +1,9 @@ +/** @type {import('./$types').PageLoad} */ +export async function load({ fetch }) { + const response = await fetch('/load/fetch-response-headers.json'); + + return { + message: await response.text(), + foo: response.headers.get('x-foo') + }; +} diff --git a/packages/kit/test/apps/basics/src/routes/load/fetch-response-headers/+page.svelte b/packages/kit/test/apps/basics/src/routes/load/fetch-response-headers/+page.svelte new file mode 100644 index 000000000000..93a90eb83100 --- /dev/null +++ b/packages/kit/test/apps/basics/src/routes/load/fetch-response-headers/+page.svelte @@ -0,0 +1,7 @@ + + +

{data.message}

+

{data.foo}

diff --git a/packages/kit/test/apps/basics/test/test.js b/packages/kit/test/apps/basics/test/test.js index 5de0f36bbf8d..f9fd21b196bc 100644 --- a/packages/kit/test/apps/basics/test/test.js +++ b/packages/kit/test/apps/basics/test/test.js @@ -825,7 +825,7 @@ test.describe('Load', () => { browserName }) => { await page.goto('/load'); - await clicknav('[href="/load/fetch-headers"]'); + await clicknav('[href="/load/fetch-request-headers"]'); const json = /** @type {string} */ (await page.textContent('pre')); const headers = JSON.parse(json); From 1ed028aef03bf0c004c4ff649629bdcdaa2f66f3 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Mon, 5 Sep 2022 12:24:48 -0400 Subject: [PATCH 6/7] argh --- .gitignore | 2 ++ ...vite.config.js.timestamp-1662392748524.mjs | 20 ------------------- 2 files changed, 2 insertions(+), 20 deletions(-) delete mode 100644 packages/kit/test/apps/amp/vite.config.js.timestamp-1662392748524.mjs diff --git a/.gitignore b/.gitignore index d005b0ecb55a..77c2b88100f7 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,7 @@ dist test-results/ package-lock.json yarn.lock +vite.config.js.timestamp-* /packages/create-svelte/template/CHANGELOG.md /packages/package/test/**/package /documentation/types.js @@ -16,3 +17,4 @@ yarn.lock .turbo .vercel .test-tmp + diff --git a/packages/kit/test/apps/amp/vite.config.js.timestamp-1662392748524.mjs b/packages/kit/test/apps/amp/vite.config.js.timestamp-1662392748524.mjs deleted file mode 100644 index 2eae59527dcd..000000000000 --- a/packages/kit/test/apps/amp/vite.config.js.timestamp-1662392748524.mjs +++ /dev/null @@ -1,20 +0,0 @@ -// vite.config.js -import * as path from "path"; -import { sveltekit } from "@sveltejs/kit/vite"; -var config = { - build: { - minify: false - }, - clearScreen: false, - plugins: [sveltekit()], - server: { - fs: { - allow: [path.resolve("../../../src")] - } - } -}; -var vite_config_default = config; -export { - vite_config_default as default -}; -//# sourceMappingURL=data:application/json;base64,ewogICJ2ZXJzaW9uIjogMywKICAic291cmNlcyI6IFsidml0ZS5jb25maWcuanMiXSwKICAic291cmNlc0NvbnRlbnQiOiBbImNvbnN0IF9fdml0ZV9pbmplY3RlZF9vcmlnaW5hbF9kaXJuYW1lID0gXCIvVXNlcnMvcmljaC9EZXZlbG9wbWVudC9TVkVMVEUvS0lUL2tpdC9wYWNrYWdlcy9raXQvdGVzdC9hcHBzL2FtcFwiO2NvbnN0IF9fdml0ZV9pbmplY3RlZF9vcmlnaW5hbF9maWxlbmFtZSA9IFwiL1VzZXJzL3JpY2gvRGV2ZWxvcG1lbnQvU1ZFTFRFL0tJVC9raXQvcGFja2FnZXMva2l0L3Rlc3QvYXBwcy9hbXAvdml0ZS5jb25maWcuanNcIjtjb25zdCBfX3ZpdGVfaW5qZWN0ZWRfb3JpZ2luYWxfaW1wb3J0X21ldGFfdXJsID0gXCJmaWxlOi8vL1VzZXJzL3JpY2gvRGV2ZWxvcG1lbnQvU1ZFTFRFL0tJVC9raXQvcGFja2FnZXMva2l0L3Rlc3QvYXBwcy9hbXAvdml0ZS5jb25maWcuanNcIjtpbXBvcnQgKiBhcyBwYXRoIGZyb20gJ3BhdGgnO1xuaW1wb3J0IHsgc3ZlbHRla2l0IH0gZnJvbSAnQHN2ZWx0ZWpzL2tpdC92aXRlJztcblxuLyoqIEB0eXBlIHtpbXBvcnQoJ3ZpdGUnKS5Vc2VyQ29uZmlnfSAqL1xuY29uc3QgY29uZmlnID0ge1xuXHRidWlsZDoge1xuXHRcdG1pbmlmeTogZmFsc2Vcblx0fSxcblx0Y2xlYXJTY3JlZW46IGZhbHNlLFxuXHRwbHVnaW5zOiBbc3ZlbHRla2l0KCldLFxuXHRzZXJ2ZXI6IHtcblx0XHRmczoge1xuXHRcdFx0YWxsb3c6IFtwYXRoLnJlc29sdmUoJy4uLy4uLy4uL3NyYycpXVxuXHRcdH1cblx0fVxufTtcblxuZXhwb3J0IGRlZmF1bHQgY29uZmlnO1xuIl0sCiAgIm1hcHBpbmdzIjogIjtBQUFxWCxZQUFZLFVBQVU7QUFDM1ksU0FBUyxpQkFBaUI7QUFHMUIsSUFBTSxTQUFTO0FBQUEsRUFDZCxPQUFPO0FBQUEsSUFDTixRQUFRO0FBQUEsRUFDVDtBQUFBLEVBQ0EsYUFBYTtBQUFBLEVBQ2IsU0FBUyxDQUFDLFVBQVUsQ0FBQztBQUFBLEVBQ3JCLFFBQVE7QUFBQSxJQUNQLElBQUk7QUFBQSxNQUNILE9BQU8sQ0FBTSxhQUFRLGNBQWMsQ0FBQztBQUFBLElBQ3JDO0FBQUEsRUFDRDtBQUNEO0FBRUEsSUFBTyxzQkFBUTsiLAogICJuYW1lcyI6IFtdCn0K From 643f6649259daca8b302a779c8e73f3f7eb6bed7 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Mon, 5 Sep 2022 12:50:19 -0400 Subject: [PATCH 7/7] fix --- .../src/runtime/server/page/serialize_data.js | 26 +++++++++---------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/packages/kit/src/runtime/server/page/serialize_data.js b/packages/kit/src/runtime/server/page/serialize_data.js index 7648eea180b2..3b278657beaf 100644 --- a/packages/kit/src/runtime/server/page/serialize_data.js +++ b/packages/kit/src/runtime/server/page/serialize_data.js @@ -44,11 +44,16 @@ export function serialize_data(fetched, filter, prerendering = false) { /** @type {Record} */ const headers = {}; + let cache_control = null; + let age = null; + for (const [key, value] of fetched.response.headers) { - const lower = key.toLowerCase(); - if (filter(lower, value)) { - headers[lower] = value; + if (filter(key, value)) { + headers[key] = value; } + + if (key === 'cache-control') cache_control = value; + if (key === 'age') age = value; } const payload = { @@ -70,16 +75,11 @@ export function serialize_data(fetched, filter, prerendering = false) { attrs.push(`data-hash=${escape_html_attr(hash(fetched.request_body))}`); } - if (!prerendering && fetched.method === 'GET') { - const cache_control = headers['cache-control']; - if (cache_control) { - const match = /s-maxage=(\d+)/g.exec(cache_control) ?? /max-age=(\d+)/g.exec(cache_control); - if (match) { - const age = headers['age'] ?? '0'; - - const ttl = +match[1] - +age; - attrs.push(`data-ttl="${ttl}"`); - } + if (!prerendering && fetched.method === 'GET' && cache_control) { + const match = /s-maxage=(\d+)/g.exec(cache_control) ?? /max-age=(\d+)/g.exec(cache_control); + if (match) { + const ttl = +match[1] - +(age ?? '0'); + attrs.push(`data-ttl="${ttl}"`); } }