From 3c964dddba08c687520ccc077519a5924d65ddcc Mon Sep 17 00:00:00 2001 From: Nic Polumeyv <162764842+Nic-Polumeyv@users.noreply.github.com> Date: Thu, 30 Jul 2026 22:39:12 -0400 Subject: [PATCH 1/4] fix: carry the tracing span in the request store instead of copying the event --- .changeset/olive-donkeys-relax.md | 5 + packages/kit/src/exports/hooks/sequence.js | 11 +- .../kit/src/exports/internal/server/event.js | 19 ++- .../kit/src/exports/internal/server/index.js | 20 +-- .../kit/src/runtime/server/page/actions.js | 9 +- .../kit/src/runtime/server/page/load_data.js | 152 +++++++++--------- .../src/runtime/server/remote-functions.js | 12 +- packages/kit/src/runtime/server/respond.js | 64 ++++---- packages/kit/src/types/internal.d.ts | 3 + packages/kit/test/apps/basics/src/app.d.ts | 1 + .../kit/test/apps/basics/src/hooks.server.js | 13 +- .../single-event-identity/+page.server.js | 5 + .../load/single-event-identity/+page.svelte | 6 + .../single-event-identity/echo/+server.js | 5 + packages/kit/test/apps/basics/test/test.js | 5 + 15 files changed, 184 insertions(+), 146 deletions(-) create mode 100644 .changeset/olive-donkeys-relax.md create mode 100644 packages/kit/test/apps/basics/src/routes/load/single-event-identity/+page.server.js create mode 100644 packages/kit/test/apps/basics/src/routes/load/single-event-identity/+page.svelte create mode 100644 packages/kit/test/apps/basics/src/routes/load/single-event-identity/echo/+server.js diff --git a/.changeset/olive-donkeys-relax.md b/.changeset/olive-donkeys-relax.md new file mode 100644 index 000000000000..aa13842c4859 --- /dev/null +++ b/.changeset/olive-donkeys-relax.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': patch +--- + +fix: keep one request event identity across `handle`, `load` and `handleFetch` diff --git a/packages/kit/src/exports/hooks/sequence.js b/packages/kit/src/exports/hooks/sequence.js index 892dc973fbd4..92462d37285a 100644 --- a/packages/kit/src/exports/hooks/sequence.js +++ b/packages/kit/src/exports/hooks/sequence.js @@ -1,9 +1,5 @@ /** @import { Handle, RequestEvent, ResolveOptions } from '@sveltejs/kit' */ -import { - merge_tracing, - get_request_store, - with_request_store -} from '@sveltejs/kit/internal/server'; +import { get_request_store, with_request_store } from '@sveltejs/kit/internal/server'; /** * A helper function for sequencing multiple `handle` calls in a middleware-like manner. @@ -99,10 +95,9 @@ export function sequence(...handlers) { name: `sveltekit.handle.sequenced.${handle.name ? handle.name : i}`, attributes: {}, fn: async (current) => { - const traced_event = merge_tracing(event, current); - return await with_request_store({ event: traced_event, state }, () => + return await with_request_store({ event, state, tracing: { current } }, () => handle({ - event: traced_event, + event, resolve: (event, options) => { /** @type {ResolveOptions['transformPageChunk']} */ const transformPageChunk = async ({ html, done }) => { diff --git a/packages/kit/src/exports/internal/server/event.js b/packages/kit/src/exports/internal/server/event.js index fe26cea22141..e67c9f149f3c 100644 --- a/packages/kit/src/exports/internal/server/event.js +++ b/packages/kit/src/exports/internal/server/event.js @@ -61,10 +61,21 @@ export function get_request_store() { return result; } -export function try_get_request_store() { +function get_raw_store() { return sync_store ?? als?.getStore() ?? null; } +/** @returns {RequestStore | null} */ +export function try_get_request_store() { + const store = get_raw_store(); + return store?.event && store.state ? store : null; +} + +/** Reads the active span through stores that hide the event, such as during `resolve` */ +export function try_get_tracing() { + return get_raw_store()?.tracing; +} + /** * @template T * @param {RequestStore | null} store @@ -72,6 +83,12 @@ export function try_get_request_store() { */ export function with_request_store(store, fn) { try { + // nested stores that don't set a span keep attributing to the active one + if (store && !store.tracing) { + const tracing = try_get_tracing(); + if (tracing) store.tracing = tracing; + } + sync_store = store; return als ? als.run(store, fn) : fn(); } finally { diff --git a/packages/kit/src/exports/internal/server/index.js b/packages/kit/src/exports/internal/server/index.js index 2fe6434ae3fe..8996a0749889 100644 --- a/packages/kit/src/exports/internal/server/index.js +++ b/packages/kit/src/exports/internal/server/index.js @@ -1,4 +1,3 @@ -/** @import { Span } from '@opentelemetry/api' */ import { try_get_request_store } from './event.js'; export function get_origin() { @@ -7,27 +6,12 @@ export function get_origin() { return request && new URL(request.url).origin; } -/** - * @template {{ tracing: { enabled: boolean, root: Span, current: Span } }} T - * @param {T} event_like - * @param {Span} current - * @returns {T} - */ -export function merge_tracing(event_like, current) { - return { - ...event_like, - tracing: { - ...event_like.tracing, - current - } - }; -} - export { with_request_store, getRequestEvent, get_request_store, - try_get_request_store + try_get_request_store, + try_get_tracing } from './event.js'; export { init_remote_functions } from './remote-functions.js'; diff --git a/packages/kit/src/runtime/server/page/actions.js b/packages/kit/src/runtime/server/page/actions.js index 315fff550560..96fce826a06e 100644 --- a/packages/kit/src/runtime/server/page/actions.js +++ b/packages/kit/src/runtime/server/page/actions.js @@ -4,7 +4,7 @@ import * as devalue from 'devalue'; import { DEV } from 'esm-env'; import { json } from '@sveltejs/kit'; import { HttpError, Redirect, ActionFailure, SvelteKitError } from '@sveltejs/kit/internal'; -import { with_request_store, merge_tracing } from '@sveltejs/kit/internal/server'; +import { with_request_store } from '@sveltejs/kit/internal/server'; import { normalize_error } from '../../../utils/error.js'; import { is_form_content_type, negotiate } from '../../../utils/http.js'; import { create_replacer, with_version_header } from '../utils.js'; @@ -281,10 +281,9 @@ async function call_action(event, event_state, actions) { '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: event_state }, () => - action(traced_event) + const result = await with_request_store( + { event, state: event_state, tracing: { current } }, + () => action(event) ); if (result instanceof ActionFailure) { diff --git a/packages/kit/src/runtime/server/page/load_data.js b/packages/kit/src/runtime/server/page/load_data.js index 67c9c3c37c07..5733a44b8d05 100644 --- a/packages/kit/src/runtime/server/page/load_data.js +++ b/packages/kit/src/runtime/server/page/load_data.js @@ -2,7 +2,7 @@ import { DEV } from 'esm-env'; import { noop } from '../../../utils/functions.js'; import { disable_search, make_trackable } from '../../../utils/url.js'; import { validate_depends, validate_load_response } from '../../shared.js'; -import { with_request_store, merge_tracing } from '@sveltejs/kit/internal/server'; +import { with_request_store } from '@sveltejs/kit/internal/server'; import { record_span } from '../../telemetry/record_span.js'; import { base64_encode } from '../../utils.js'; import { NULL_BODY_STATUS } from '../constants.js'; @@ -82,94 +82,95 @@ export async function load_server_data({ event, event_state, state, node, parent '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: 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)) { - console.warn( - `${node.server_id}: Calling \`event.fetch(...)\` in a promise handler after \`load(...)\` has returned will not cause the function to re-run when 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); + const result = await with_request_store( + { event, state: event_state, tracing: { current } }, + () => + load.call(null, { + ...event, + fetch: (info, init) => { + const url = new URL(info instanceof Request ? info.url : info, event.url); + + if (DEV && done && !uses.dependencies.has(url.href)) { + console.warn( + `${node.server_id}: Calling \`event.fetch(...)\` in a promise handler after \`load(...)\` has returned will not cause the function to re-run when the dependency is invalidated` + ); + } - if (DEV) { - validate_depends(node.server_id || 'missing route ID', dep); + // 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)) { + console.warn( + `${node.server_id}: Calling \`depends(...)\` in a promise handler after \`load(...)\` has returned will not cause the function to re-run when the dependency is invalidated` + ); + } + } - if (done && !uses.dependencies.has(href)) { + uses.dependencies.add(href); + } + }, + params: new Proxy(event.params, { + get: (target, key) => { + if (DEV && done && typeof key === 'string' && !uses.params.has(key)) { console.warn( - `${node.server_id}: Calling \`depends(...)\` in a promise handler after \`load(...)\` has returned will not cause the function to re-run when the dependency is invalidated` + `${node.server_id}: Accessing \`params.${String( + key + )}\` in a promise handler after \`load(...)\` has returned will not cause the function to re-run when the param changes` ); } - } - uses.dependencies.add(href); - } - }, - params: new Proxy(event.params, { - get: (target, key) => { - if (DEV && done && typeof key === 'string' && !uses.params.has(key)) { + if (is_tracking) { + uses.params.add(key); + } + return target[/** @type {string} */ (key)]; + } + }), + parent: async () => { + if (DEV && done && !uses.parent) { console.warn( - `${node.server_id}: Accessing \`params.${String( - key - )}\` in a promise handler after \`load(...)\` has returned will not cause the function to re-run when the param changes` + `${node.server_id}: Calling \`parent(...)\` in a promise handler after \`load(...)\` has returned will not cause the function to re-run when parent data changes` ); } if (is_tracking) { - uses.params.add(key); + uses.parent = true; } - return target[/** @type {string} */ (key)]; - } - }), - parent: async () => { - if (DEV && done && !uses.parent) { - console.warn( - `${node.server_id}: Calling \`parent(...)\` in a promise handler after \`load(...)\` has returned will not cause the function to re-run when parent data changes` - ); - } + return parent(); + }, + route: new Proxy(event.route, { + get: (target, key) => { + if (DEV && done && typeof key === 'string' && !uses.route) { + console.warn( + `${node.server_id}: Accessing \`route.${String( + key + )}\` in a promise handler after \`load(...)\` has returned will not cause the function to re-run when the route 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) { - console.warn( - `${node.server_id}: Accessing \`route.${String( - key - )}\` in a promise handler after \`load(...)\` has returned will not cause the function to re-run when the route changes` - ); + if (is_tracking) { + uses.route = true; + } + return target[/** @type {'id'} */ (key)]; } - - if (is_tracking) { - uses.route = true; + }), + url, + untrack(fn) { + is_tracking = false; + try { + return fn(); + } finally { + is_tracking = true; } - return target[/** @type {'id'} */ (key)]; } - }), - url, - untrack(fn) { - is_tracking = false; - try { - return fn(); - } finally { - is_tracking = true; - } - } - }) + }) ); return result; @@ -233,10 +234,9 @@ export async function load_data({ 'http.route': event.route.id || 'unknown' }, fn: async (current) => { - const traced_event = merge_tracing(event, current); const child_state = { ...event_state, is_in_universal_load: true }; - return await with_request_store({ event: traced_event, state: child_state }, () => + return await with_request_store({ event, state: child_state, tracing: { current } }, () => load.call(null, { url: event.url, params: event.params, @@ -247,7 +247,7 @@ export async function load_data({ depends: noop, parent, untrack: (fn) => fn(), - tracing: traced_event.tracing + tracing: event.tracing }) ); } diff --git a/packages/kit/src/runtime/server/remote-functions.js b/packages/kit/src/runtime/server/remote-functions.js index 08e0f988aaa5..ac50bf105f17 100644 --- a/packages/kit/src/runtime/server/remote-functions.js +++ b/packages/kit/src/runtime/server/remote-functions.js @@ -3,7 +3,7 @@ import { json, error } from '@sveltejs/kit'; import { Redirect, SvelteKitError } from '@sveltejs/kit/internal'; -import { with_request_store, merge_tracing } from '@sveltejs/kit/internal/server'; +import { with_request_store } from '@sveltejs/kit/internal/server'; import { app_dir, base } from '$app/paths/internal/server'; import { is_form_content_type } from '../../utils/http.js'; import { create_remote_key, parse_remote_arg, split_remote_key, stringify } from '../shared.js'; @@ -30,9 +30,8 @@ export async function handle_remote_call(event, state, options, manifest, id) { 'sveltekit.remote.call.id': id }, fn: async (current) => { - const traced_event = merge_tracing(event, current); - const response = await with_request_store({ event: traced_event, state }, () => - handle_remote_call_internal(traced_event, state, options, manifest, id) + const response = await with_request_store({ event, state, tracing: { current } }, () => + handle_remote_call_internal(event, state, options, manifest, id) ); return with_version_header(response); } @@ -521,9 +520,8 @@ export async function handle_remote_form_post(event, state, manifest, id) { 'sveltekit.remote.form.post.id': id }, fn: (current) => { - const traced_event = merge_tracing(event, current); - return with_request_store({ event: traced_event, state }, () => - handle_remote_form_post_internal(traced_event, state, manifest, id) + return with_request_store({ event, state, tracing: { current } }, () => + handle_remote_form_post_internal(event, state, manifest, id) ); } }); diff --git a/packages/kit/src/runtime/server/respond.js b/packages/kit/src/runtime/server/respond.js index a575adae62ac..5fffa7d4bdf5 100644 --- a/packages/kit/src/runtime/server/respond.js +++ b/packages/kit/src/runtime/server/respond.js @@ -2,7 +2,7 @@ import { DEV } from 'esm-env'; import { json, text } from '@sveltejs/kit'; import { Redirect, SvelteKitError } from '@sveltejs/kit/internal'; -import { merge_tracing, with_request_store } from '@sveltejs/kit/internal/server'; +import { try_get_tracing, with_request_store } from '@sveltejs/kit/internal/server'; import { base, app_dir } from '$app/paths/internal/server'; import { is_endpoint_request, render_endpoint } from './endpoint.js'; import { render_page } from './page/index.js'; @@ -484,30 +484,35 @@ export async function internal_respond(request, options, manifest, state) { 'sveltekit.is_sub_request': event.isSubRequest }, fn: async (root_span) => { - const traced_event = { - ...event, - tracing: { - enabled: __SVELTEKIT_SERVER_TRACING_ENABLED__, - root: root_span, - current: root_span + const tracing = { + enabled: __SVELTEKIT_SERVER_TRACING_ENABLED__, + root: root_span, + get current() { + return try_get_tracing()?.current ?? root_span; } }; - - return await with_request_store({ event: traced_event, state: event_state }, () => - options.hooks.handle({ - event: traced_event, - resolve: (event, opts) => { - return record_span({ - name: 'sveltekit.resolve', - attributes: { - 'http.route': event.route.id || 'unknown' - }, - fn: (resolve_span) => { - // counter-intuitively, we need to clear the event, so that it's not - // e.g. accessible when loading modules needed to handle the request - return with_request_store(null, () => - resolve(merge_tracing(event, resolve_span), page_nodes, opts).then( - (response) => { + event.tracing = tracing; + + return await with_request_store( + { event, state: event_state, tracing: { current: root_span } }, + () => + options.hooks.handle({ + event, + resolve: (event, opts) => { + return record_span({ + name: 'sveltekit.resolve', + attributes: { + 'http.route': event.route.id || 'unknown' + }, + fn: (resolve_span) => { + // counter-intuitively, we need to clear the event, so that it's not + // e.g. accessible when loading modules needed to handle the request + const tracing_store = /** @type {import('types').RequestStore} */ ({ + tracing: { current: resolve_span } + }); + + return with_request_store(tracing_store, () => + resolve(event, page_nodes, opts).then((response) => { // add headers/cookies here, rather than inside `resolve`, so that we // can do it once for all responses instead of once per `return` for (const key in headers) { @@ -528,13 +533,12 @@ export async function internal_respond(request, options, manifest, state) { }); return response; - } - ) - ); - } - }); - } - }) + }) + ); + } + }); + } + }) ); } }); diff --git a/packages/kit/src/types/internal.d.ts b/packages/kit/src/types/internal.d.ts index 3d5c723af32b..b09fe360bcef 100644 --- a/packages/kit/src/types/internal.d.ts +++ b/packages/kit/src/types/internal.d.ts @@ -748,6 +748,9 @@ export interface RequestState { export interface RequestStore { event: RequestEvent; state: RequestState; + tracing?: { + current: Span; + }; } /** Type of the `__sveltekit_abc123` object in the init ` + +

{data.written_in_handle}

diff --git a/packages/kit/test/apps/basics/src/routes/load/single-event-identity/echo/+server.js b/packages/kit/test/apps/basics/src/routes/load/single-event-identity/echo/+server.js new file mode 100644 index 000000000000..cef0f2b68b2f --- /dev/null +++ b/packages/kit/test/apps/basics/src/routes/load/single-event-identity/echo/+server.js @@ -0,0 +1,5 @@ +import { json } from '@sveltejs/kit'; + +export function GET({ request }) { + return json({ written_in_handle: request.headers.get('x-written-in-handle') }); +} diff --git a/packages/kit/test/apps/basics/test/test.js b/packages/kit/test/apps/basics/test/test.js index de04d97a9331..388698f7e05c 100644 --- a/packages/kit/test/apps/basics/test/test.js +++ b/packages/kit/test/apps/basics/test/test.js @@ -477,6 +477,11 @@ test.describe('Load', () => { expect(await page.textContent('h1')).toBe('the answer is 42'); }); + test('handleFetch sees changes made to the event in handle', async ({ page }) => { + await page.goto('/load/single-event-identity'); + expect(await page.textContent('h1')).toBe('yes'); + }); + test('makes credentialed fetches to endpoints by default', async ({ page, clicknav, From d46938d4fcdf68bb30adfb306440e12dc57a9960 Mon Sep 17 00:00:00 2001 From: Nic Polumeyv <162764842+Nic-Polumeyv@users.noreply.github.com> Date: Thu, 30 Jul 2026 23:12:09 -0400 Subject: [PATCH 2/4] chore: declare the tracing-only store shape, reuse existing test fixtures --- .../kit/src/exports/internal/server/event.js | 10 +- .../kit/src/runtime/server/page/load_data.js | 145 +++++++++--------- packages/kit/src/runtime/server/respond.js | 14 +- packages/kit/src/types/internal.d.ts | 9 ++ packages/kit/test/apps/basics/src/app.d.ts | 1 - .../kit/test/apps/basics/src/hooks.server.js | 14 +- .../via-fetch/+page.server.js | 5 + .../get-request-event/via-fetch/+page.svelte | 5 + .../single-event-identity/+page.server.js | 5 - .../load/single-event-identity/+page.svelte | 6 - .../single-event-identity/echo/+server.js | 5 - packages/kit/test/apps/basics/test/test.js | 10 +- 12 files changed, 111 insertions(+), 118 deletions(-) create mode 100644 packages/kit/test/apps/basics/src/routes/get-request-event/via-fetch/+page.server.js create mode 100644 packages/kit/test/apps/basics/src/routes/get-request-event/via-fetch/+page.svelte delete mode 100644 packages/kit/test/apps/basics/src/routes/load/single-event-identity/+page.server.js delete mode 100644 packages/kit/test/apps/basics/src/routes/load/single-event-identity/+page.svelte delete mode 100644 packages/kit/test/apps/basics/src/routes/load/single-event-identity/echo/+server.js diff --git a/packages/kit/src/exports/internal/server/event.js b/packages/kit/src/exports/internal/server/event.js index e67c9f149f3c..057fb151d617 100644 --- a/packages/kit/src/exports/internal/server/event.js +++ b/packages/kit/src/exports/internal/server/event.js @@ -1,12 +1,12 @@ /** @import { RequestEvent } from '@sveltejs/kit' */ -/** @import { RequestStore } from 'types' */ +/** @import { RequestStore, TracingStore } from 'types' */ /** @import { AsyncLocalStorage } from 'node:async_hooks' */ import { IN_WEBCONTAINER } from '../../../runtime/server/constants.js'; -/** @type {RequestStore | null} */ +/** @type {RequestStore | TracingStore | null} */ let sync_store = null; -/** @type {AsyncLocalStorage | null} */ +/** @type {AsyncLocalStorage | null} */ let als; import('node:async_hooks') @@ -68,7 +68,7 @@ function get_raw_store() { /** @returns {RequestStore | null} */ export function try_get_request_store() { const store = get_raw_store(); - return store?.event && store.state ? store : null; + return store?.event ? store : null; } /** Reads the active span through stores that hide the event, such as during `resolve` */ @@ -78,7 +78,7 @@ export function try_get_tracing() { /** * @template T - * @param {RequestStore | null} store + * @param {RequestStore | TracingStore | null} store * @param {() => T} fn */ export function with_request_store(store, fn) { diff --git a/packages/kit/src/runtime/server/page/load_data.js b/packages/kit/src/runtime/server/page/load_data.js index 5733a44b8d05..5d0e81419381 100644 --- a/packages/kit/src/runtime/server/page/load_data.js +++ b/packages/kit/src/runtime/server/page/load_data.js @@ -82,95 +82,94 @@ export async function load_server_data({ event, event_state, state, node, parent 'http.route': event.route.id || 'unknown' }, fn: async (current) => { - const result = await with_request_store( - { event, state: event_state, tracing: { current } }, - () => - load.call(null, { - ...event, - fetch: (info, init) => { - const url = new URL(info instanceof Request ? info.url : info, event.url); - - if (DEV && done && !uses.dependencies.has(url.href)) { - console.warn( - `${node.server_id}: Calling \`event.fetch(...)\` in a promise handler after \`load(...)\` has returned will not cause the function to re-run when the dependency is invalidated` - ); - } + const store = { event, state: event_state, tracing: { current } }; + const result = await with_request_store(store, () => + load.call(null, { + ...event, + fetch: (info, init) => { + const url = new URL(info instanceof Request ? info.url : info, event.url); + + if (DEV && done && !uses.dependencies.has(url.href)) { + console.warn( + `${node.server_id}: Calling \`event.fetch(...)\` in a promise handler after \`load(...)\` has returned will not cause the function to re-run when 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)) { - console.warn( - `${node.server_id}: Calling \`depends(...)\` in a promise handler after \`load(...)\` has returned will not cause the function to re-run when 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); - uses.dependencies.add(href); - } - }, - params: new Proxy(event.params, { - get: (target, key) => { - if (DEV && done && typeof key === 'string' && !uses.params.has(key)) { + if (DEV) { + validate_depends(node.server_id || 'missing route ID', dep); + + if (done && !uses.dependencies.has(href)) { console.warn( - `${node.server_id}: Accessing \`params.${String( - key - )}\` in a promise handler after \`load(...)\` has returned will not cause the function to re-run when the param changes` + `${node.server_id}: Calling \`depends(...)\` in a promise handler after \`load(...)\` has returned will not cause the function to re-run when the dependency is invalidated` ); } - - if (is_tracking) { - uses.params.add(key); - } - return target[/** @type {string} */ (key)]; } - }), - parent: async () => { - if (DEV && done && !uses.parent) { + + uses.dependencies.add(href); + } + }, + params: new Proxy(event.params, { + get: (target, key) => { + if (DEV && done && typeof key === 'string' && !uses.params.has(key)) { console.warn( - `${node.server_id}: Calling \`parent(...)\` in a promise handler after \`load(...)\` has returned will not cause the function to re-run when parent data changes` + `${node.server_id}: Accessing \`params.${String( + key + )}\` in a promise handler after \`load(...)\` has returned will not cause the function to re-run when the param changes` ); } if (is_tracking) { - uses.parent = true; + uses.params.add(key); } - return parent(); - }, - route: new Proxy(event.route, { - get: (target, key) => { - if (DEV && done && typeof key === 'string' && !uses.route) { - console.warn( - `${node.server_id}: Accessing \`route.${String( - key - )}\` in a promise handler after \`load(...)\` has returned will not cause the function to re-run when the route changes` - ); - } + return target[/** @type {string} */ (key)]; + } + }), + parent: async () => { + if (DEV && done && !uses.parent) { + console.warn( + `${node.server_id}: Calling \`parent(...)\` in a promise handler after \`load(...)\` has returned will not cause the function to re-run when parent data changes` + ); + } - if (is_tracking) { - uses.route = true; - } - return target[/** @type {'id'} */ (key)]; + if (is_tracking) { + uses.parent = true; + } + return parent(); + }, + route: new Proxy(event.route, { + get: (target, key) => { + if (DEV && done && typeof key === 'string' && !uses.route) { + console.warn( + `${node.server_id}: Accessing \`route.${String( + key + )}\` in a promise handler after \`load(...)\` has returned will not cause the function to re-run when the route changes` + ); } - }), - url, - untrack(fn) { - is_tracking = false; - try { - return fn(); - } finally { - is_tracking = true; + + 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; diff --git a/packages/kit/src/runtime/server/respond.js b/packages/kit/src/runtime/server/respond.js index 5fffa7d4bdf5..9be21c1c66ee 100644 --- a/packages/kit/src/runtime/server/respond.js +++ b/packages/kit/src/runtime/server/respond.js @@ -204,6 +204,8 @@ export async function internal_respond(request, options, manifest, state) { cookies, // @ts-expect-error `fetch` needs to be created after the `event` itself fetch: null, + // @ts-expect-error `tracing` needs the root span, which is created during `handle` + tracing: null, getClientAddress: state.getClientAddress || (() => { @@ -484,15 +486,15 @@ export async function internal_respond(request, options, manifest, state) { 'sveltekit.is_sub_request': event.isSubRequest }, fn: async (root_span) => { - const tracing = { + event.tracing = { enabled: __SVELTEKIT_SERVER_TRACING_ENABLED__, root: root_span, get current() { return try_get_tracing()?.current ?? root_span; } }; - event.tracing = tracing; + // the explicit span stops sub-requests from inheriting the parent request's span return await with_request_store( { event, state: event_state, tracing: { current: root_span } }, () => @@ -505,13 +507,9 @@ export async function internal_respond(request, options, manifest, state) { 'http.route': event.route.id || 'unknown' }, fn: (resolve_span) => { - // counter-intuitively, we need to clear the event, so that it's not + // counter-intuitively, we need to hide the event, so that it's not // e.g. accessible when loading modules needed to handle the request - const tracing_store = /** @type {import('types').RequestStore} */ ({ - tracing: { current: resolve_span } - }); - - return with_request_store(tracing_store, () => + return with_request_store({ tracing: { current: resolve_span } }, () => resolve(event, page_nodes, opts).then((response) => { // add headers/cookies here, rather than inside `resolve`, so that we // can do it once for all responses instead of once per `return` diff --git a/packages/kit/src/types/internal.d.ts b/packages/kit/src/types/internal.d.ts index b09fe360bcef..b9aaf3a92b24 100644 --- a/packages/kit/src/types/internal.d.ts +++ b/packages/kit/src/types/internal.d.ts @@ -753,6 +753,15 @@ export interface RequestStore { }; } +/** Carries the active span through scopes that must hide the event, such as `resolve` */ +export interface TracingStore { + event?: undefined; + state?: undefined; + tracing: { + current: Span; + }; +} + /** Type of the `__sveltekit_abc123` object in the init ` + +

{data.message}

diff --git a/packages/kit/test/apps/basics/src/routes/load/single-event-identity/+page.server.js b/packages/kit/test/apps/basics/src/routes/load/single-event-identity/+page.server.js deleted file mode 100644 index 9778e59adcd7..000000000000 --- a/packages/kit/test/apps/basics/src/routes/load/single-event-identity/+page.server.js +++ /dev/null @@ -1,5 +0,0 @@ -/** @type {import('./$types').PageServerLoad} */ -export async function load({ fetch }) { - const res = await fetch('/load/single-event-identity/echo'); - return await res.json(); -} diff --git a/packages/kit/test/apps/basics/src/routes/load/single-event-identity/+page.svelte b/packages/kit/test/apps/basics/src/routes/load/single-event-identity/+page.svelte deleted file mode 100644 index 5d4122bbcccb..000000000000 --- a/packages/kit/test/apps/basics/src/routes/load/single-event-identity/+page.svelte +++ /dev/null @@ -1,6 +0,0 @@ - - -

{data.written_in_handle}

diff --git a/packages/kit/test/apps/basics/src/routes/load/single-event-identity/echo/+server.js b/packages/kit/test/apps/basics/src/routes/load/single-event-identity/echo/+server.js deleted file mode 100644 index cef0f2b68b2f..000000000000 --- a/packages/kit/test/apps/basics/src/routes/load/single-event-identity/echo/+server.js +++ /dev/null @@ -1,5 +0,0 @@ -import { json } from '@sveltejs/kit'; - -export function GET({ request }) { - return json({ written_in_handle: request.headers.get('x-written-in-handle') }); -} diff --git a/packages/kit/test/apps/basics/test/test.js b/packages/kit/test/apps/basics/test/test.js index 388698f7e05c..0161053bc7ac 100644 --- a/packages/kit/test/apps/basics/test/test.js +++ b/packages/kit/test/apps/basics/test/test.js @@ -477,11 +477,6 @@ test.describe('Load', () => { expect(await page.textContent('h1')).toBe('the answer is 42'); }); - test('handleFetch sees changes made to the event in handle', async ({ page }) => { - await page.goto('/load/single-event-identity'); - expect(await page.textContent('h1')).toBe('yes'); - }); - test('makes credentialed fetches to endpoints by default', async ({ page, clicknav, @@ -1609,6 +1604,11 @@ test.describe('getRequestEvent', () => { await page.goto('/get-request-event/with-error'); expect(await page.textContent('h1')).toBe('Crashing now (500 hello from hooks.server.js)'); }); + + test('handleFetch sees what handle wrote to the event', async ({ page }) => { + await page.goto('/get-request-event/via-fetch'); + expect(await page.textContent('h1')).toBe('hello from hooks.server.js'); + }); }); test.describe('params prop', () => { From ba9fc1dacf00c6eeb806c2e3813ac74795bfc9f3 Mon Sep 17 00:00:00 2001 From: Nic Polumeyv <162764842+Nic-Polumeyv@users.noreply.github.com> Date: Sat, 1 Aug 2026 18:31:11 -0400 Subject: [PATCH 3/4] read the current span from opentelemetry instead of carrying it in the request store --- packages/kit/src/exports/hooks/sequence.js | 7 +- .../kit/src/exports/internal/server/event.js | 27 ++---- .../kit/src/exports/internal/server/index.js | 3 +- .../kit/src/runtime/server/page/actions.js | 5 +- .../kit/src/runtime/server/page/load_data.js | 14 ++- .../src/runtime/server/remote-functions.js | 11 ++- packages/kit/src/runtime/server/respond.js | 89 +++++++++---------- packages/kit/src/runtime/telemetry/otel.js | 10 ++- packages/kit/src/types/internal.d.ts | 12 --- .../routes/tracing/current/+page.server.js | 12 +++ .../src/routes/tracing/current/+page.svelte | 5 ++ .../kit/test/apps/basics/test/server.test.js | 28 +++++- 12 files changed, 113 insertions(+), 110 deletions(-) create mode 100644 packages/kit/test/apps/basics/src/routes/tracing/current/+page.server.js create mode 100644 packages/kit/test/apps/basics/src/routes/tracing/current/+page.svelte diff --git a/packages/kit/src/exports/hooks/sequence.js b/packages/kit/src/exports/hooks/sequence.js index 92462d37285a..b569a07f4daf 100644 --- a/packages/kit/src/exports/hooks/sequence.js +++ b/packages/kit/src/exports/hooks/sequence.js @@ -94,8 +94,8 @@ export function sequence(...handlers) { return state.tracing.record_span({ name: `sveltekit.handle.sequenced.${handle.name ? handle.name : i}`, attributes: {}, - fn: async (current) => { - return await with_request_store({ event, state, tracing: { current } }, () => + fn: async () => + with_request_store({ event, state }, () => handle({ event, resolve: (event, options) => { @@ -133,8 +133,7 @@ export function sequence(...handlers) { }); } }) - ); - } + ) }); } }; diff --git a/packages/kit/src/exports/internal/server/event.js b/packages/kit/src/exports/internal/server/event.js index 057fb151d617..fe26cea22141 100644 --- a/packages/kit/src/exports/internal/server/event.js +++ b/packages/kit/src/exports/internal/server/event.js @@ -1,12 +1,12 @@ /** @import { RequestEvent } from '@sveltejs/kit' */ -/** @import { RequestStore, TracingStore } from 'types' */ +/** @import { RequestStore } from 'types' */ /** @import { AsyncLocalStorage } from 'node:async_hooks' */ import { IN_WEBCONTAINER } from '../../../runtime/server/constants.js'; -/** @type {RequestStore | TracingStore | null} */ +/** @type {RequestStore | null} */ let sync_store = null; -/** @type {AsyncLocalStorage | null} */ +/** @type {AsyncLocalStorage | null} */ let als; import('node:async_hooks') @@ -61,34 +61,17 @@ export function get_request_store() { return result; } -function get_raw_store() { - return sync_store ?? als?.getStore() ?? null; -} - -/** @returns {RequestStore | null} */ export function try_get_request_store() { - const store = get_raw_store(); - return store?.event ? store : null; -} - -/** Reads the active span through stores that hide the event, such as during `resolve` */ -export function try_get_tracing() { - return get_raw_store()?.tracing; + return sync_store ?? als?.getStore() ?? null; } /** * @template T - * @param {RequestStore | TracingStore | null} store + * @param {RequestStore | null} store * @param {() => T} fn */ export function with_request_store(store, fn) { try { - // nested stores that don't set a span keep attributing to the active one - if (store && !store.tracing) { - const tracing = try_get_tracing(); - if (tracing) store.tracing = tracing; - } - sync_store = store; return als ? als.run(store, fn) : fn(); } finally { diff --git a/packages/kit/src/exports/internal/server/index.js b/packages/kit/src/exports/internal/server/index.js index 8996a0749889..557eda303c18 100644 --- a/packages/kit/src/exports/internal/server/index.js +++ b/packages/kit/src/exports/internal/server/index.js @@ -10,8 +10,7 @@ export { with_request_store, getRequestEvent, get_request_store, - try_get_request_store, - try_get_tracing + try_get_request_store } from './event.js'; export { init_remote_functions } from './remote-functions.js'; diff --git a/packages/kit/src/runtime/server/page/actions.js b/packages/kit/src/runtime/server/page/actions.js index 96fce826a06e..7aa203e5487a 100644 --- a/packages/kit/src/runtime/server/page/actions.js +++ b/packages/kit/src/runtime/server/page/actions.js @@ -281,10 +281,7 @@ async function call_action(event, event_state, actions) { 'http.route': event.route.id || 'unknown' }, fn: async (current) => { - const result = await with_request_store( - { event, state: event_state, tracing: { current } }, - () => action(event) - ); + const result = await with_request_store({ event, state: event_state }, () => action(event)); if (result instanceof ActionFailure) { current.setAttributes({ diff --git a/packages/kit/src/runtime/server/page/load_data.js b/packages/kit/src/runtime/server/page/load_data.js index 5d0e81419381..3d9d8027d63a 100644 --- a/packages/kit/src/runtime/server/page/load_data.js +++ b/packages/kit/src/runtime/server/page/load_data.js @@ -81,9 +81,8 @@ export async function load_server_data({ event, event_state, state, node, parent 'sveltekit.load.environment': 'server', 'http.route': event.route.id || 'unknown' }, - fn: async (current) => { - const store = { event, state: event_state, tracing: { current } }; - const result = await with_request_store(store, () => + fn: async () => + with_request_store({ event, state: event_state }, () => load.call(null, { ...event, fetch: (info, init) => { @@ -170,10 +169,7 @@ export async function load_server_data({ event, event_state, state, node, parent } } }) - ); - - return result; - } + ) }); if (DEV) { @@ -232,10 +228,10 @@ export async function load_data({ 'sveltekit.load.environment': 'server', 'http.route': event.route.id || 'unknown' }, - fn: async (current) => { + fn: async () => { const child_state = { ...event_state, is_in_universal_load: true }; - return await with_request_store({ event, state: child_state, tracing: { current } }, () => + return await with_request_store({ event, state: child_state }, () => load.call(null, { url: event.url, params: event.params, diff --git a/packages/kit/src/runtime/server/remote-functions.js b/packages/kit/src/runtime/server/remote-functions.js index ac50bf105f17..d99180e0bc92 100644 --- a/packages/kit/src/runtime/server/remote-functions.js +++ b/packages/kit/src/runtime/server/remote-functions.js @@ -29,8 +29,8 @@ export async function handle_remote_call(event, state, options, manifest, id) { attributes: { 'sveltekit.remote.call.id': id }, - fn: async (current) => { - const response = await with_request_store({ event, state, tracing: { current } }, () => + fn: async () => { + const response = await with_request_store({ event, state }, () => handle_remote_call_internal(event, state, options, manifest, id) ); return with_version_header(response); @@ -519,11 +519,10 @@ export async function handle_remote_form_post(event, state, manifest, id) { attributes: { 'sveltekit.remote.form.post.id': id }, - fn: (current) => { - return with_request_store({ event, state, tracing: { current } }, () => + fn: () => + with_request_store({ event, state }, () => handle_remote_form_post_internal(event, state, manifest, id) - ); - } + ) }); } diff --git a/packages/kit/src/runtime/server/respond.js b/packages/kit/src/runtime/server/respond.js index 9be21c1c66ee..2ad4ae9ae985 100644 --- a/packages/kit/src/runtime/server/respond.js +++ b/packages/kit/src/runtime/server/respond.js @@ -2,7 +2,7 @@ import { DEV } from 'esm-env'; import { json, text } from '@sveltejs/kit'; import { Redirect, SvelteKitError } from '@sveltejs/kit/internal'; -import { try_get_tracing, with_request_store } from '@sveltejs/kit/internal/server'; +import { with_request_store } from '@sveltejs/kit/internal/server'; import { base, app_dir } from '$app/paths/internal/server'; import { is_endpoint_request, render_endpoint } from './endpoint.js'; import { render_page } from './page/index.js'; @@ -39,7 +39,7 @@ import { import { server_data_serializer } from './page/data_serializer.js'; import { get_remote_id, handle_remote_call } from './remote-functions.js'; import { record_span } from '../telemetry/record_span.js'; -import { otel } from '../telemetry/otel.js'; +import { otel, trace } from '../telemetry/otel.js'; /** @type {import('types').RequiredResolveOptions['transformPageChunk']} */ const default_transform = ({ html }) => html; @@ -490,53 +490,50 @@ export async function internal_respond(request, options, manifest, state) { enabled: __SVELTEKIT_SERVER_TRACING_ENABLED__, root: root_span, get current() { - return try_get_tracing()?.current ?? root_span; + return trace?.getActiveSpan() ?? root_span; } }; - // the explicit span stops sub-requests from inheriting the parent request's span - return await with_request_store( - { event, state: event_state, tracing: { current: root_span } }, - () => - options.hooks.handle({ - event, - resolve: (event, opts) => { - return record_span({ - name: 'sveltekit.resolve', - attributes: { - 'http.route': event.route.id || 'unknown' - }, - fn: (resolve_span) => { - // counter-intuitively, we need to hide the event, so that it's not - // e.g. accessible when loading modules needed to handle the request - return with_request_store({ tracing: { current: resolve_span } }, () => - resolve(event, page_nodes, opts).then((response) => { - // add headers/cookies here, rather than inside `resolve`, so that we - // can do it once for all responses instead of once per `return` - for (const key in headers) { - const value = headers[key]; - response.headers.set(key, /** @type {string} */ (value)); - } - - add_cookies_to_headers(response.headers, new_cookies.values()); - - if (state.prerendering && event.route.id !== null) { - response.headers.set('x-sveltekit-routeid', encodeURI(event.route.id)); - } - - resolve_span.setAttributes({ - 'http.response.status_code': response.status, - 'http.response.body.size': - response.headers.get('content-length') || 'unknown' - }); - - return response; - }) - ); - } - }); - } - }) + return await with_request_store({ event, state: event_state }, () => + options.hooks.handle({ + event, + resolve: (event, opts) => { + return record_span({ + name: 'sveltekit.resolve', + attributes: { + 'http.route': event.route.id || 'unknown' + }, + fn: (resolve_span) => { + // counter-intuitively, we need to clear the event, so that it's not + // e.g. accessible when loading modules needed to handle the request + return with_request_store(null, () => + resolve(event, page_nodes, opts).then((response) => { + // add headers/cookies here, rather than inside `resolve`, so that we + // can do it once for all responses instead of once per `return` + for (const key in headers) { + const value = headers[key]; + response.headers.set(key, /** @type {string} */ (value)); + } + + add_cookies_to_headers(response.headers, new_cookies.values()); + + if (state.prerendering && event.route.id !== null) { + response.headers.set('x-sveltekit-routeid', encodeURI(event.route.id)); + } + + resolve_span.setAttributes({ + 'http.response.status_code': response.status, + 'http.response.body.size': + response.headers.get('content-length') || 'unknown' + }); + + return response; + }) + ); + } + }); + } + }) ); } }); diff --git a/packages/kit/src/runtime/telemetry/otel.js b/packages/kit/src/runtime/telemetry/otel.js index a293423ebade..1b92b4a2682c 100644 --- a/packages/kit/src/runtime/telemetry/otel.js +++ b/packages/kit/src/runtime/telemetry/otel.js @@ -1,11 +1,19 @@ -/** @import { Tracer, SpanStatusCode, PropagationAPI, ContextAPI } from '@opentelemetry/api' */ +/** @import { Tracer, SpanStatusCode, PropagationAPI, ContextAPI, TraceAPI } from '@opentelemetry/api' */ /** @type {Promise<{ tracer: Tracer, SpanStatusCode: typeof SpanStatusCode, propagation: PropagationAPI, context: ContextAPI }> | null} */ export let otel = null; +/** + * Synchronously readable once `otel` has resolved, which is guaranteed + * before any span exists — `record_span` awaits `otel` first. + * @type {TraceAPI | null} + */ +export let trace = null; + if (__SVELTEKIT_SERVER_TRACING_ENABLED__) { otel = import('@opentelemetry/api') .then((module) => { + trace = module.trace; return { tracer: module.trace.getTracer('sveltekit'), propagation: module.propagation, diff --git a/packages/kit/src/types/internal.d.ts b/packages/kit/src/types/internal.d.ts index b9aaf3a92b24..3d5c723af32b 100644 --- a/packages/kit/src/types/internal.d.ts +++ b/packages/kit/src/types/internal.d.ts @@ -748,18 +748,6 @@ export interface RequestState { export interface RequestStore { event: RequestEvent; state: RequestState; - tracing?: { - current: Span; - }; -} - -/** Carries the active span through scopes that must hide the event, such as `resolve` */ -export interface TracingStore { - event?: undefined; - state?: undefined; - tracing: { - current: Span; - }; } /** Type of the `__sveltekit_abc123` object in the init ` + +

{data.ok}

diff --git a/packages/kit/test/apps/basics/test/server.test.js b/packages/kit/test/apps/basics/test/server.test.js index 1eef97fc5bf9..dd5a0927a6b4 100644 --- a/packages/kit/test/apps/basics/test/server.test.js +++ b/packages/kit/test/apps/basics/test/server.test.js @@ -1088,22 +1088,25 @@ test.describe('$app/env', () => { }); test.describe('tracing', () => { - // Helper function to find the resolve.root span deep in the handle.child chain /** * @param {import('../../../types.js').SpanTree} span + * @param {(span: import('../../../types.js').SpanTree) => boolean} predicate * @returns {import('../../../types.js').SpanTree | null} */ - function find_resolve_root_span(span) { - if (span.name === 'sveltekit.resolve') { + function find_span(span, predicate) { + if (predicate(span)) { return span; } for (const child of span.children || []) { - const found = find_resolve_root_span(child); + const found = find_span(child, predicate); if (found) return found; } return null; } + /** @param {import('../../../types.js').SpanTree} span */ + const find_resolve_root_span = (span) => find_span(span, (s) => s.name === 'sveltekit.resolve'); + function rand() { // node 18 doesn't have crypto.randomUUID() and we run tests in node 18 return Math.random().toString(36).substring(2, 15); @@ -1199,6 +1202,23 @@ test.describe('tracing', () => { }); }); + test('tracing.current in a server load is the load span', async ({ page, read_traces }) => { + const test_id = rand(); + await page.goto(`/tracing/current?test_id=${test_id}`); + const traces = read_traces(test_id); + expect(traces.length).toBeGreaterThan(0); + + // the attributes set via tracing.current land on the load span, not the root + const load_span = find_span( + traces[0], + (s) => s.attributes['sveltekit.load.node_id'] === 'src/routes/tracing/current/+page.server.js' + ); + expect(load_span).not.toBeNull(); + expect(load_span?.attributes.current_matches_otel).toBe(true); + expect(load_span?.attributes.current_matches_otel_after_await).toBe(true); + expect(traces[0].attributes.current_matches_otel).toBeUndefined(); + }); + test('correct spans are created for HttpError', async ({ page, read_traces }) => { const test_id = rand(); const response = await page.goto(`/tracing/http-error?test_id=${test_id}`); From 080c26a596f20712fd874b26cf03874a3884dc5f Mon Sep 17 00:00:00 2001 From: Nic Polumeyv <162764842+Nic-Polumeyv@users.noreply.github.com> Date: Sat, 1 Aug 2026 19:31:46 -0400 Subject: [PATCH 4/4] stop forking the event for data requests --- packages/kit/src/runtime/server/data/index.js | 12 ++---------- packages/kit/src/runtime/server/respond.js | 6 ++++-- packages/kit/test/apps/basics/src/hooks.server.js | 2 +- .../get-request-event/via-data/+page.server.js | 10 ++++++++++ .../routes/get-request-event/via-data/+page.svelte | 5 +++++ packages/kit/test/apps/basics/test/test.js | 12 ++++++++++++ 6 files changed, 34 insertions(+), 13 deletions(-) create mode 100644 packages/kit/test/apps/basics/src/routes/get-request-event/via-data/+page.server.js create mode 100644 packages/kit/test/apps/basics/src/routes/get-request-event/via-data/+page.svelte diff --git a/packages/kit/src/runtime/server/data/index.js b/packages/kit/src/runtime/server/data/index.js index e65fbe5c0230..215af27cb858 100644 --- a/packages/kit/src/runtime/server/data/index.js +++ b/packages/kit/src/runtime/server/data/index.js @@ -5,7 +5,6 @@ import { once } from '../../../utils/functions.js'; import { server_data_serializer_json } from '../page/data_serializer.js'; import { load_server_data } from '../page/load_data.js'; import { handle_error_and_jsonify } from '../errors.js'; -import { normalize_path } from '../../../utils/url.js'; import { text_encoder } from '../../utils.js'; import { with_version_header } from '../utils.js'; @@ -17,7 +16,6 @@ import { with_version_header } from '../utils.js'; * @param {import('@sveltejs/kit').SSRManifest} manifest * @param {import('types').SSRState} state * @param {boolean[] | undefined} invalidated_data_nodes - * @param {import('types').TrailingSlash} trailing_slash * @returns {Promise} */ export async function render_data( @@ -27,8 +25,7 @@ export async function render_data( options, manifest, state, - invalidated_data_nodes, - trailing_slash + invalidated_data_nodes ) { if (!route.page) { // requesting /__data.json should fail for a +server.js @@ -41,11 +38,6 @@ export async function render_data( let aborted = false; - const url = new URL(event.url); - url.pathname = normalize_path(url.pathname, trailing_slash); - - const new_event = { ...event, url }; - const functions = node_ids.map((n, i) => { return once(async () => { try { @@ -59,7 +51,7 @@ export async function render_data( const node = n == undefined ? n : await manifest._.nodes[n](); // load this. for the child, return as is. for the final result, stream things return load_server_data({ - event: new_event, + event, event_state, state, node, diff --git a/packages/kit/src/runtime/server/respond.js b/packages/kit/src/runtime/server/respond.js index 2ad4ae9ae985..b3783065ae50 100644 --- a/packages/kit/src/runtime/server/respond.js +++ b/packages/kit/src/runtime/server/respond.js @@ -423,6 +423,9 @@ export async function internal_respond(request, options, manifest, state) { } }); } + } else { + // a data request can't follow a redirect, so normalize the path in place + url.pathname = normalize_path(url.pathname, trailing_slash); } if (state.before_handle || state.emulator?.platform) { @@ -654,8 +657,7 @@ export async function internal_respond(request, options, manifest, state) { options, manifest, state, - invalidated_data_nodes, - trailing_slash + invalidated_data_nodes ); } else { let endpoint; diff --git a/packages/kit/test/apps/basics/src/hooks.server.js b/packages/kit/test/apps/basics/src/hooks.server.js index b67732022aa0..df4ad98a4b1b 100644 --- a/packages/kit/test/apps/basics/src/hooks.server.js +++ b/packages/kit/test/apps/basics/src/hooks.server.js @@ -206,7 +206,7 @@ export const handle = sequence( /** @type {import('@sveltejs/kit').HandleFetch} */ export async function handleFetch({ event, request, fetch }) { - if (event.url.pathname === '/get-request-event/via-fetch') { + if (event.url.pathname.startsWith('/get-request-event/via-')) { request.headers.set('x-message', event.locals.message ?? 'missing'); } diff --git a/packages/kit/test/apps/basics/src/routes/get-request-event/via-data/+page.server.js b/packages/kit/test/apps/basics/src/routes/get-request-event/via-data/+page.server.js new file mode 100644 index 000000000000..6e412e919fb3 --- /dev/null +++ b/packages/kit/test/apps/basics/src/routes/get-request-event/via-data/+page.server.js @@ -0,0 +1,10 @@ +import { getRequestEvent } from '$app/server'; + +/** @type {import('./$types').PageServerLoad} */ +export async function load({ fetch }) { + const event = getRequestEvent(); + // reassignment, not mutation: only visible in handleFetch if the store event is never a copy + event.locals = { ...event.locals, message: 'hello from the server load' }; + const res = await fetch('/headers/echo'); + return { message: (await res.json())['x-message'] }; +} diff --git a/packages/kit/test/apps/basics/src/routes/get-request-event/via-data/+page.svelte b/packages/kit/test/apps/basics/src/routes/get-request-event/via-data/+page.svelte new file mode 100644 index 000000000000..59b61bf1447a --- /dev/null +++ b/packages/kit/test/apps/basics/src/routes/get-request-event/via-data/+page.svelte @@ -0,0 +1,5 @@ + + +

{data.message}

diff --git a/packages/kit/test/apps/basics/test/test.js b/packages/kit/test/apps/basics/test/test.js index 0161053bc7ac..cc1ede4dd4ee 100644 --- a/packages/kit/test/apps/basics/test/test.js +++ b/packages/kit/test/apps/basics/test/test.js @@ -1609,6 +1609,18 @@ test.describe('getRequestEvent', () => { await page.goto('/get-request-event/via-fetch'); expect(await page.textContent('h1')).toBe('hello from hooks.server.js'); }); + + test('handleFetch sees what a server load wrote during a data request', async ({ + app, + page, + javaScriptEnabled + }) => { + if (!javaScriptEnabled) return; + + await page.goto('/get-request-event/via-fetch'); + await app.goto('/get-request-event/via-data'); + expect(await page.textContent('h1')).toBe('hello from the server load'); + }); }); test.describe('params prop', () => {