UNPKG

@sveltejs/kit

Version:

SvelteKit is the fastest way to build Svelte apps

510 lines (440 loc) • 13.9 kB
import { DEV } from 'esm-env'; import { noop } from '../../../utils/functions.js'; import { disable_search, make_trackable } from '../../../utils/url.js'; import { fetch_cache_url, validate_depends, validate_load_response } from '../../shared.js'; import { with_request_store, merge_tracing, record_span } from '@sveltejs/kit/internal/server'; import { base64_encode } from '../../utils.js'; import { NULL_BODY_STATUS } from '../constants.js'; import { get_node_type } from '../utils.js'; import * as e from '../../../messages/server-errors.js'; import * as w from '../../../messages/server-warnings.js'; /** * Calls the user's server `load` function. * @param {{ * event: import('@sveltejs/kit').RequestEvent; * state: import('types').RequestState; * node: import('types').SSRNode | undefined; * parent: () => Promise<Record<string, any>>; * }} opts * @returns {Promise<import('types').ServerDataNode | null>} */ export async function load_server_data({ event, state, node, parent }) { if (!node?.server) return null; let is_tracking = true; const uses = { dependencies: new Set(), params: new Set(), parent: false, route: false, url: false, search_params: new Set() }; const load = node.server.load; // TODO: shouldn't this be calculated using PageNodes? there could be a trailingSlash option on a layout const slash = node.server.trailingSlash; if (!load) { return { type: 'data', data: null, uses, slash }; } const url = make_trackable( event.url, () => { if (DEV && done && !uses.url) { w.load_tracking_after_return({ id: /** @type {string} */ (node.server_id), usage: 'Accessing URL properties', change: 'the URL changes' }); } if (is_tracking) { uses.url = true; } }, (param) => { if (DEV && done && !uses.search_params.has(param)) { w.load_tracking_after_return({ id: /** @type {string} */ (node.server_id), usage: 'Accessing URL properties', change: 'the URL changes' }); } if (is_tracking) { uses.search_params.add(param); } } ); if (state.prerendering || state.prerender_default === true) { disable_search(url); } let done = false; const result = await record_span({ name: 'sveltekit.load', attributes: { 'sveltekit.load.node_id': node.server_id || 'unknown', 'sveltekit.load.node_type': get_node_type(node.server_id), 'sveltekit.load.environment': 'server', 'http.route': event.route.id || 'unknown' }, fn: async (current) => { const traced_event = merge_tracing(event, current); const result = await with_request_store({ event: traced_event, state }, () => load.call(null, { ...traced_event, fetch: (info, init) => { const url = new URL(info instanceof Request ? info.url : info, event.url); if (DEV && done && !uses.dependencies.has(url.href)) { w.load_tracking_after_return({ id: /** @type {string} */ (node.server_id), usage: 'Calling `event.fetch(...)`', change: 'the dependency is invalidated' }); } // Note: server fetches are not added to uses.depends due to security concerns return event.fetch(info, init); }, /** @param {string[]} deps */ depends: (...deps) => { for (const dep of deps) { const { href } = new URL(dep, event.url); if (DEV) { validate_depends(node.server_id || 'missing route ID', dep); if (done && !uses.dependencies.has(href)) { w.load_tracking_after_return({ id: /** @type {string} */ (node.server_id), usage: 'Calling `depends(...)`', change: 'the dependency is invalidated' }); } } uses.dependencies.add(href); } }, params: new Proxy(event.params, { get: (target, key) => { if (DEV && done && typeof key === 'string' && !uses.params.has(key)) { w.load_tracking_after_return({ id: /** @type {string} */ (node.server_id), usage: `Accessing \`params.${key}\``, change: 'the param changes' }); } if (is_tracking) { uses.params.add(key); } return target[/** @type {string} */ (key)]; } }), parent: async () => { if (DEV && done && !uses.parent) { w.load_tracking_after_return({ id: /** @type {string} */ (node.server_id), usage: 'Calling `parent(...)`', change: 'parent data changes' }); } if (is_tracking) { uses.parent = true; } return parent(); }, route: new Proxy(event.route, { get: (target, key) => { if (DEV && done && typeof key === 'string' && !uses.route) { w.load_tracking_after_return({ id: /** @type {string} */ (node.server_id), usage: `Accessing \`route.${key}\``, change: 'the route changes' }); } if (is_tracking) { uses.route = true; } return target[/** @type {'id'} */ (key)]; } }), url, untrack(fn) { is_tracking = false; try { return fn(); } finally { is_tracking = true; } } }) ); return result; } }); if (DEV) { validate_load_response(result, `in ${node.server_id}`); } done = true; return { type: 'data', data: result ?? null, uses, slash }; } /** * Calls the user's `load` function. * @param {{ * event: import('@sveltejs/kit').RequestEvent; * state: import('types').RequestState; * fetched: import('./types.js').Fetched[]; * node: import('types').SSRNode | undefined; * parent: () => Promise<Record<string, any>>; * resolve_opts: import('types').RequiredResolveOptions; * server_data_promise: Promise<import('types').ServerDataNode | null>; * csr: boolean; * }} opts * @returns {Promise<Record<string, any | Promise<any>> | null>} */ export async function load_data({ event, state, fetched, node, parent, server_data_promise, resolve_opts, csr }) { const server_data_node = await server_data_promise; const load = node?.universal?.load; if (!load) { return server_data_node?.data ?? null; } const result = await record_span({ name: 'sveltekit.load', attributes: { 'sveltekit.load.node_id': node.universal_id || 'unknown', 'sveltekit.load.node_type': get_node_type(node.universal_id), 'sveltekit.load.environment': 'server', 'http.route': event.route.id || 'unknown' }, fn: async (current) => { const traced_event = merge_tracing(event, current); return await with_request_store({ event: traced_event, state }, () => load.call(null, { url: event.url, params: event.params, data: server_data_node?.data ?? null, route: event.route, fetch: create_universal_fetch(event, state.prerendering, fetched, csr, resolve_opts), setHeaders: event.setHeaders, depends: noop, parent, untrack: (fn) => fn(), tracing: traced_event.tracing }) ); } }); if (DEV) { validate_load_response(result, `in ${node.universal_id}`); } return result ?? null; } /** * @param {Pick<import('@sveltejs/kit').RequestEvent, 'fetch' | 'url' | 'request' | 'route'>} event * @param {import('types').PrerenderOptions | undefined} prerendering * @param {import('./types.js').Fetched[]} fetched * @param {boolean} csr * @param {Pick<Required<import('@sveltejs/kit/hooks').ResolveOptions>, 'filterSerializedResponseHeaders'>} resolve_opts * @returns {typeof fetch} */ export function create_universal_fetch(event, prerendering, fetched, csr, resolve_opts) { /** * @param {URL | RequestInfo} input * @param {RequestInit} [init] */ const universal_fetch = async (input, init) => { const cloned_body = input instanceof Request && input.body ? input.clone().body : null; const cloned_headers = input instanceof Request && [...input.headers].length ? new Headers(input.headers) : init?.headers; let response = await event.fetch(input, init); const url = new URL(input instanceof Request ? input.url : input, event.url); const same_origin = url.origin === event.url.origin; /** @type {import('types').PrerenderDependency} */ let dependency; if (same_origin) { if (prerendering) { dependency = { response, body: null }; prerendering.dependencies.set(url.pathname, dependency); } } else if (url.protocol === 'https:' || url.protocol === 'http:') { // simulate CORS errors and "no access to body in no-cors mode" server-side for consistency with client-side behaviour const mode = input instanceof Request ? input.mode : (init?.mode ?? 'cors'); if (mode === 'no-cors') { response = new Response('', { status: response.status, statusText: response.statusText, headers: response.headers }); } else { const acao = response.headers.get('access-control-allow-origin'); if (!acao || (acao !== event.url.origin && acao !== '*')) { e.load_fetch_cors({ reason: acao ? 'Incorrect' : 'No' }); } } } /** @type {ReadableStream<Uint8Array>} */ let teed_body; const proxy = new Proxy(response, { get(response, key, receiver) { /** * @param {string | undefined} body * @param {boolean} is_b64 */ async function push_fetched(body, is_b64) { const status_number = Number(response.status); if (isNaN(status_number)) { throw new Error( `response.status is not a number. value: "${ response.status }" type: ${typeof response.status}` ); } const request_body = input instanceof Request && cloned_body ? await new Response(cloned_body).text() : init?.body; if ( request_body && typeof request_body !== 'string' && !ArrayBuffer.isView(request_body) ) { // requests whose body can't be hashed aren't serialized — the browser repeats the fetch return; } fetched.push({ url: fetch_cache_url(url, event.url), method: event.request.method, request_body: /** @type {string | ArrayBufferView | null | undefined} */ (request_body), request_headers: cloned_headers, response_body: body, response, is_b64 }); } if (key === 'body') { if (response.body === null) { return null; } if (teed_body) { return teed_body; } const [a, b] = response.body.tee(); void (async () => { const result = new Uint8Array(await new Response(a).arrayBuffer()); if (dependency) { dependency.body = new Uint8Array(result); } void push_fetched(base64_encode(result), true); })().catch(noop); // prevent unhandled rejection potentially crashing the process return (teed_body = b); } if (key === 'arrayBuffer') { return async () => { const buffer = await response.arrayBuffer(); const bytes = new Uint8Array(buffer); if (dependency) { dependency.body = bytes; } if (buffer instanceof ArrayBuffer) { await push_fetched(base64_encode(bytes), true); } return buffer; }; } async function text() { const body = await response.text(); if (body === '' && NULL_BODY_STATUS.includes(response.status)) { await push_fetched(undefined, false); return undefined; } if (!body || typeof body === 'string') { await push_fetched(body, false); } if (dependency) { dependency.body = body; } return body; } if (key === 'text') { return text; } if (key === 'json') { return async () => { const body = await text(); return body ? JSON.parse(body) : undefined; }; } const value = Reflect.get(response, key, response); if (value instanceof Function) { // On Node v24+, the Response object has a private element #state – we // need to bind this function to the response in order to allow it to // access this private element. Defining the name and length ensure it // is identical to the original function when introspected. return Object.defineProperties( /** * @this {any} */ function () { return Reflect.apply(value, this === receiver ? response : this, arguments); }, { name: { value: value.name }, length: { value: value.length } } ); } return value; } }); if (csr) { // 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) { e.load_response_header_not_serialized({ name: lower, id: /** @type {string} */ (event.route.id) }); } } return value; }; const get_set_cookie = response.headers.getSetCookie; response.headers.getSetCookie = () => { const values = get_set_cookie.call(response.headers); for (const value of values) { const included = resolve_opts.filterSerializedResponseHeaders('set-cookie', value); if (!included) { e.load_response_header_not_serialized({ name: 'set-cookie', id: /** @type {string} */ (event.route.id) }); } } return values; }; } return proxy; }; // Don't make this function `async`! Otherwise, the user has to `catch` promises they use for streaming responses or else // it will be an unhandled rejection. Instead, we add a `.catch(noop)` ourselves below to this from happening. return (input, init) => { // See docs in fetch.js for why we need to do this const response = universal_fetch(input, init); response.catch(noop); return response; }; }