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', () => {